5-2. 컨테이너 오케스트레이션 도구, 쿠버네티스 - 파트 2
- 본문의 "실습" 블록은 모두 실제로 실행한 명령어와 실제 출력임 (2026-10-15 실행)
- 환경: Windows 11 + Docker Desktop 내장 쿠버네티스 (멀티 노드 구성)
| 항목 | 값 |
|---|---|
| 쿠버네티스 버전 | 서버 v1.36.1 / kubectl v1.34.0 |
| 노드 | desktop-control-plane 1대 + desktop-worker, desktop-worker2 2대 |
| 컨테이너 런타임 | containerd 2.3.1 |
| kube-proxy 모드 | iptables |
- 파드 이름 끝의 랜덤 문자열(
-6r4vr등), IP 주소, AGE는 실행할 때마다 달라짐 - Docker Desktop의 기본 설정(단일 노드)이라면 노드가
docker-desktop1대로만 보이고, 나머지 결과는 거의 같음 - 명령어는 Git Bash 기준. Git Bash는
/etc/...처럼/로 시작하는 인자를 윈도 경로(C:/Program Files/Git/etc/...)로 바꿔 버리므로, 먼저export MSYS_NO_PATHCONV=1을 실행해 두면 안전함 (파워셸·리눅스·맥OS 터미널은 필요 없음)
# 실습 전 확인: 지금 어느 클러스터를 조작하는지 확인 (CURRENT에 * 표시)
kubectl config get-contexts
# 출력:
# CURRENT NAME CLUSTER AUTHINFO NAMESPACE
# * docker-desktop docker-desktop docker-desktop
# 클러스터를 구성하는 노드 확인
kubectl get nodes
# 출력:
# NAME STATUS ROLES AGE VERSION
# desktop-control-plane Ready control-plane 14d v1.36.1
# desktop-worker Ready <none> 14d v1.36.1
# desktop-worker2 Ready <none> 14d v1.36.1
Warning: version difference between client (1.34) and server (1.36) exceeds the supported minor version skew of +/-1
→ kubectl과 클러스터의 버전 차이가 2단계 이상이라는 경고. 기본 명령은 대부분 동작하지만, kubectl을 클러스터 버전에 맞춰 업데이트하는 것이 좋음
5.7. 로드 밸런싱과 헬스 체크: 시스템의 가용성을 향상시키는 방법
5.7.1. 로드 밸런싱과 헬스 체크란
- 로드 밸런싱(Load Balancing): 여러 컨테이너에 요청을 분배해 부하를 분산하는 것
- 헬스 체크(Health Check): 시스템을 구성하는 컨테이너가 정상적으로 실행 중인지 감시하는 것
- 두 가지를 조합해 정상적으로 실행되지 않는 컨테이너에는 요청을 분배하지 않도록 하면, 시스템이 정상적으로 가동되는 시간을 늘릴 수 있음
- 로드 밸런싱과 헬스 체크를 이야기할 때 함께 등장하는 개념이 가용성임
- 로드 밸런싱 = 번호표 기계. 손님(요청)을 비어 있는 창구(컨테이너)로 골고루 보냄
- 헬스 체크 = 창구 직원이 자리에 있는지 확인하는 매니저
- 둘을 합치면: 자리를 비운 창구로는 번호표를 배정하지 않음 → 손님은 계속 업무를 볼 수 있음 (= 가용성 유지)
5.7.2. 가용성이란
- 가용성(Availability): 사용자가 시스템이나 서비스를 사용하고 싶을 때 사용할 수 있는 정도
- 가용성을 평가하는 지표 중 하나가 가동률임
가동률 계산식:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
시스템이 정상적으로 가동된 시간
가동률 = ─────────────────────────────────
시스템이 가동된 시간
예) 30일(720시간) 중 7.2시간 장애 → (720 - 7.2) / 720 = 99%
- 가동률은 흔히 "나인(9)의 개수" 로 표현함. 99%와 99.9%는 0.9% 차이 같지만 허용 장애 시간은 10배 차이
| 가동률 | 별칭 | 1년 중 허용되는 장애 시간 |
|---|---|---|
| 99% | 투 나인 | 약 3.65일 |
| 99.9% | 쓰리 나인 | 약 8.76시간 |
| 99.99% | 포 나인 | 약 52.6분 |
| 99.999% | 파이브 나인 | 약 5.26분 |
- 가용성을 높이는 방법 중 하나는 단일 장애 지점(SPOF)을 제거하는 것
- 단일 장애 지점: 시스템의 구성 요소 중 여기에 장애가 발생하면 전체 시스템이 중단되는 지점
- 단일 장애 지점을 제거하는 방법 중 하나가 이중화
- 이중화: 예비를 준비해 두고, 고장이 나면 예비와 교환해 시스템 전체가 멈추는 것을 방지하는 것
- 로드 밸런싱과 헬스 체크는 고장 발견과 예비로의 교체를 자동화해 가용성 향상에 기여함
단일 장애 지점 제거 = 이중화 + 자동 교체:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[이중화 전: 단일 장애 지점] [이중화 후]
요청 요청
│ │
▼ ▼
┌──────────┐ [LB + 헬스 체크]
│ 컨테이너 │ ← 고장 = 전체 중단 ┌───┴────────┐
└──────────┘ ▼ ▼
┌────────┐ ┌────────┐
│컨테이너1│ │컨테이너2│
└────────┘ └────────┘
✗ 고장 → 요청을 2로만 전송
(자동 발견) (자동 교체)
- 가용성 (Availability): 사용자가 원할 때 시스템이나 서비스를 사용할 수 있는 정도
- 가동률: 정상 가동 시간 ÷ 전체 가동 시간. 가용성 평가 지표의 하나
- 단일 장애 지점 (SPOF, Single Point of Failure): 장애가 나면 전체 시스템이 중단되는 구성 요소
- 이중화 (Redundancy): 예비를 준비해 두고 고장 시 교체해 시스템 중단을 막는 방법
5.7.3. 쿠버네티스의 로드 밸런싱
- 쿠버네티스는 서비스(Service) 라는 방식으로 로드 밸런싱을 실시함 (→ 5-1의 5.5절)
- 클러스터 내에 서비스를 생성하면 엔드포인트가 되는 IP 주소가 생성됨
- 이 IP 주소를 대상으로 하는 패킷은 서비스 뒤에 있는 파드로 전달됨
- 서비스 뒤에 여러 파드가 있으면 순서대로 각 파드에 패킷을 전달 → 이 전송 방법을 라운드 로빈(Round Robin) 이라고 함
- 패킷 전달에는 iptables의 네트워크 주소 변환(NAT) 기능을 사용함
- 패킷 전송 규칙은 kube-proxy라는 에이전트 프로그램이 필요에 따라 변경함
- iptables 대신 IPVS를 사용할 수도 있음 → 서비스 수가 10,000개를 초과하는 대규모 클러스터에서는 IPVS가 권장됨
- 파드는 언제든 죽고 새로 만들어지는 소모품이고, 새로 만들어질 때마다 IP 주소가 바뀜
- 실제로 아래 실습에서 롤링 업데이트(5.10.4) 전후로 파드 IP가 모두 바뀌었음
- 서비스는 변하지 않는 IP와 이름(
my-service) 을 제공하고, 뒤의 파드 목록은 쿠버네티스가 자동으로 최신 상태로 유지함 - 비유: 직원(파드)이 바뀌어도 대표 전화번호(서비스) 는 그대로
서비스의 패킷 전달 구조:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[클라이언트]
│ 서비스 IP(엔드포인트)로 패킷 전송
▼
┌──────────────────────────────────────┐
│ 노드의 netfilter │
│ (iptables 또는 IPVS 규칙으로 NAT) │ ← kube-proxy가 규칙을 갱신
└──────────┬───────────────────────────┘
┌──────┼───────────┐
▼ ▼ ▼
[파드 A] [파드 B] [파드 C]
iptables와 IPVS 비교:
| 구분 | iptables | IPVS |
|---|---|---|
| 정체 | 리눅스의 네트워크 관리 도구 | 리눅스 커널에 내장된 로드 밸런싱 기능 |
| netfilter 사용 | ○ (netfilter 설정을 변경) | ○ |
| 규칙 보관 방식 | 규칙 목록을 위에서부터 차례로 검사 | 해시 테이블에 보존 (바로 찾아감) |
| 동작 | - | 커널 모드에서 동작 |
| 속도 | 서비스가 많아질수록 느려짐 | iptables보다 빠름 |
| 적합한 규모 | 일반 클러스터 | 서비스 10,000개 초과 대규모 클러스터 |
5.9.6~5.9.7에서 만드는 my-deployment(nginx 파드 3개)와 my-service를 사용함. 각 파드가 응답에 자기 이름을 넣도록 바꾼 뒤, 클러스터 안의 테스트용 파드에서 서비스로 10번 요청을 보냄
# 1) 클러스터 안에서 요청을 보낼 테스트용 파드(client) 생성
kubectl run client --image=curlimages/curl --restart=Never --command -- sleep 3600
# 2) 각 nginx 파드의 첫 페이지를 "Hello from <파드 이름>"으로 변경
# (Git Bash/리눅스 기준. $(...)는 명령 결과를 끼워 넣는 문법)
for p in $(kubectl get pods -l app=my-app -o name); do
kubectl exec $p -- sh -c 'echo "Hello from $(hostname)" > /usr/share/nginx/html/index.html'
done
# 3) 서비스 이름으로 10번 요청
kubectl exec client -- sh -c 'for i in 1 2 3 4 5 6 7 8 9 10; do curl -s http://my-service:8080; done'
실제 출력:
Hello from my-deployment-5f9c57bbf7-k5slg
Hello from my-deployment-5f9c57bbf7-9w5sz
Hello from my-deployment-5f9c57bbf7-k5slg
Hello from my-deployment-5f9c57bbf7-c4wvz
Hello from my-deployment-5f9c57bbf7-c4wvz
Hello from my-deployment-5f9c57bbf7-c4wvz
Hello from my-deployment-5f9c57bbf7-c4wvz
Hello from my-deployment-5f9c57bbf7-9w5sz
Hello from my-deployment-5f9c57bbf7-9w5sz
Hello from my-deployment-5f9c57bbf7-c4wvz
- 결과 읽기
- 같은 주소(
my-service:8080)로 보냈는데 3개의 파드가 나눠서 응답함 → 로드 밸런싱 동작 확인 - 그런데 순서가
k5slg → 9w5sz → c4wvz → ...처럼 정확히 돌아가며 응답하지는 않음. 같은 파드가 4번 연속 응답하기도 함 - 즉 책의 설명(순서대로 = 라운드 로빈)과 달리, iptables 모드는 무작위로 파드를 고름. 요청이 많아지면 결과적으로 거의 균등해짐
- 같은 주소(
- 서비스 이름
my-service로 접속할 수 있는 것은 클러스터 내부 DNS(CoreDNS) 덕분임
kubectl exec client -- nslookup my-service.default.svc.cluster.local
# 출력:
# Server: 10.96.0.10 ← 클러스터 DNS 서버(CoreDNS)의 주소
# Name: my-service.default.svc.cluster.local
# Address: 10.96.103.142 ← 서비스의 가상 IP(ClusterIP)
- 서비스의 정식 이름은
<서비스>.<네임스페이스>.svc.cluster.local이고, 같은 네임스페이스 안에서는my-service만 써도 됨
kube-proxy 파드 안에는 iptables 도구가 있으므로, 노드에 설정된 NAT 규칙 중 my-service 관련 줄만 뽑아볼 수 있음
# desktop-worker 노드에서 동작하는 kube-proxy 파드 찾기
kubectl get pods -n kube-system -l k8s-app=kube-proxy -o wide
# 그 kube-proxy 파드 안에서 NAT 테이블 규칙 출력 → my-service 관련 줄만 필터
kubectl exec -n kube-system kube-proxy-gpv8r -- sh -c 'iptables-save -t nat | grep "default/my-service"'
실제 출력 (핵심 줄만, 보기 쉽게 정렬):
① 서비스 IP:포트로 온 패킷을 my-service 전용 체인으로 보냄
-A KUBE-SERVICES -d 10.96.103.142/32 -p tcp --dport 8080 -j KUBE-SVC-I37Z43XJW6BD4TLV
② 전용 체인에서 확률로 파드 하나를 고름
-A KUBE-SVC-I37Z43XJW6BD4TLV ... -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-Y67ZTPCC2FTAXVBS
-A KUBE-SVC-I37Z43XJW6BD4TLV ... -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-RVD7P5CVIZTJGFR2
-A KUBE-SVC-I37Z43XJW6BD4TLV ... -j KUBE-SEP-RSNVYYY6S75AZ3QJ
③ 고른 파드의 실제 IP:80으로 목적지 주소를 바꿈 (DNAT)
-A KUBE-SEP-Y67ZTPCC2FTAXVBS -p tcp -j DNAT --to-destination 10.244.1.4:80
-A KUBE-SEP-RVD7P5CVIZTJGFR2 -p tcp -j DNAT --to-destination 10.244.2.6:80
-A KUBE-SEP-RSNVYYY6S75AZ3QJ -p tcp -j DNAT --to-destination 10.244.2.7:80
규칙 해석: 왜 확률이 1/3, 1/2, 나머지일까?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
패킷 도착 (목적지 10.96.103.142:8080)
│
▼ 규칙1: 1/3 확률로 당첨?
├─ 예 (1/3) ──────────────────────→ 파드 10.244.1.4
│ 아니오 (2/3)
▼ 규칙2: 남은 것 중 1/2 확률로 당첨?
├─ 예 (2/3 × 1/2 = 1/3) ──────────→ 파드 10.244.2.6
│ 아니오 (1/3)
▼ 규칙3: 무조건
└─ (1/3) ─────────────────────────→ 파드 10.244.2.7
→ 위에서부터 차례로 검사하므로 확률을 1/3, 1/2, 1로 두면
세 파드가 각각 정확히 1/3씩 선택됨 (= 무작위 균등 분배)
- KUBE-SVC-...: 서비스 하나당 하나 생기는 체인 (SVC = Service)
- KUBE-SEP-...: 파드(엔드포인트) 하나당 하나 생기는 체인 (SEP = Service EndPoint)
- DNAT: 패킷의 목적지 주소를 바꾸는 것 → 서비스 IP가 파드 IP로 바뀜 (5-1의 "가상 IP + NAT"가 바로 이것)
- 파드가 늘거나 줄면 kube-proxy가 이 규칙을 자동으로 다시 씀. 규칙이 "위에서부터 차례로 검사"되므로 서비스가 수만 개가 되면 느려짐 → IPVS(해시 테이블)가 필요한 이유
- 라운드 로빈 (Round Robin): 여러 대상에 순서대로 하나씩 돌아가며 요청을 분배하는 방식
- netfilter: 패킷 필터링과 네트워크 주소 변환을 수행하는 리눅스 커널의 기능
- iptables: netfilter 설정을 변경하는 리눅스의 네트워크 관리 도구
- IPVS (IP Virtual Server): 리눅스 커널에 내장된 로드 밸런싱 기능. 해시 테이블·커널 모드로 iptables보다 빠름
- kube-proxy: 각 노드에서 동작하며 서비스의 패킷 전송 규칙을 갱신하는 에이전트
- DNAT (Destination NAT): 패킷의 목적지 주소를 바꾸는 네트워크 주소 변환
- CoreDNS: 서비스 이름을 IP 주소로 바꿔 주는 클러스터 내부 DNS 서버
- 실습에서 확인했듯 iptables 모드는 확률(무작위)로 파드를 선택함. 순서대로 돌리는 라운드 로빈은 IPVS 모드의 기본 스케줄러(
rr)에 해당 - kube-proxy에는 iptables·IPVS 외에 nftables 모드가 추가되어 v1.33에서 GA가 됨. 반면 IPVS 모드는 v1.35에서 지원 중단(deprecated)이 예고되어, 대규모 클러스터 대안으로는 nftables 모드가 권장되는 추세
- Cilium 같은 eBPF 기반 CNI가 kube-proxy를 아예 대체하는 구성도 많이 쓰임
5.7.4. 쿠버네티스의 헬스 체크
- 클러스터를 구성하는 각 시스템(노드)에는 kubelet이라는 에이전트 프로그램이 있음
- kubelet은 같은 시스템에서 실행 중인 파드의 상태를 정기적으로 확인함
- 정상·비정상을 판단하는 방법
| 방법 | 동작 | 판단 기준 |
|---|---|---|
| 명령어 실행 (exec) | 파드의 컨테이너 안에서 지정된 명령어 실행 | 명령어 성공 여부 (종료 코드 0이면 성공) |
| TCP (tcpSocket) | 지정 포트로 TCP 접속 시도 | 접속 성공 여부 |
| HTTP (httpGet) | 지정 경로로 HTTP 요청 | 응답 코드가 200~399면 성공 |
- 판단 결과에 따라 파드를 다시 시작하거나 서비스에서 요청 전송을 중지해 클러스터를 정상 상태로 유지함
- 실제로는 이 두 동작을 서로 다른 프로브(Probe) 가 담당함
| 프로브 | 질문 | 실패하면 |
|---|---|---|
| livenessProbe | "살아 있니?" | 컨테이너를 재시작 |
| readinessProbe | "손님 받을 준비 됐니?" | 서비스의 전송 대상에서 제외 (재시작은 안 함) |
| startupProbe | "다 켜졌니?" | 기동이 끝날 때까지 다른 프로브를 보류 |
아님. 프로세스는 떠 있지만 내부가 꼬여서(교착 상태, 메모리 누수 등) 요청에 응답하지 못하는 경우가 흔함. 그래서 "프로세스가 있나?"가 아니라 "실제로 일을 하나?" 를 물어보는 것이 헬스 체크의 핵심
kubelet의 헬스 체크 흐름:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[kubelet] ──정기 확인──→ [파드의 컨테이너]
│ ├─ 명령어 실행
│ ├─ TCP 접속
│ └─ HTTP 요청
▼
성공? ──예──→ 정상 → 그대로 유지
│
아니오
▼
비정상 ─┬─→ (liveness 실패) 컨테이너 재시작
└─→ (readiness 실패) 서비스의 요청 전송 대상에서 제외
시작하면 /tmp/healthy 파일을 만들고, 30초 뒤 스스로 지워 버리는 컨테이너. kubelet은 5초마다 cat /tmp/healthy로 파일이 있는지 확인함
# liveness.yaml
apiVersion: v1
kind: Pod
metadata:
name: liveness-demo
spec:
containers:
- name: app
image: busybox:1.36
# 파일 생성 → 30초 대기 → 파일 삭제(고장 흉내) → 계속 대기
args: ["sh", "-c", "touch /tmp/healthy; sleep 30; rm -f /tmp/healthy; sleep 600"]
livenessProbe:
exec:
command: ["cat", "/tmp/healthy"] # 파일이 없으면 명령 실패 = 비정상
initialDelaySeconds: 5 # 시작 후 5초 뒤부터 검사
periodSeconds: 5 # 5초마다 검사
kubectl apply -f liveness.yaml
kubectl describe pod liveness-demo # 맨 아래 Events 확인
실제 출력 (약 50초 후, Events 부분):
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Started 50s kubelet Container started
Warning Unhealthy 9s (x3 over 19s) kubelet Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
Normal Killing 9s kubelet Container app failed liveness probe, will be restarted
kubectl get pod liveness-demo
# 실제 출력:
# NAME READY STATUS RESTARTS AGE
# liveness-demo 1/1 Running 1 (2s ago) 77s
- 결과 읽기
Unhealthy ... (x3 over 19s): 검사가 3번 연속 실패함 (기본failureThreshold가 3)Killing ... will be restarted: kubelet이 컨테이너를 죽이고 다시 시작함RESTARTS 1: 재시작 횟수. 다시 시작된 컨테이너는/tmp/healthy를 새로 만들므로 다시 정상이 됨 → 사람이 개입하지 않아도 스스로 복구
my-service가 고르는 레이블(app: my-app)을 붙인 nginx 파드를 하나 더 만들되, /ready.html 페이지가 있어야만 준비 완료로 판단하게 함. 처음에는 이 파일이 없음
# readiness.yaml
apiVersion: v1
kind: Pod
metadata:
name: readiness-demo
labels:
app: my-app # my-service의 셀렉터와 같은 레이블
spec:
containers:
- name: nginx
image: nginx:stable
readinessProbe:
httpGet:
path: /ready.html # 이 페이지가 200을 돌려줘야 준비 완료
port: 80
periodSeconds: 3
kubectl apply -f readiness.yaml
kubectl get pod readiness-demo
# 실제 출력:
# NAME READY STATUS RESTARTS AGE
# readiness-demo 0/1 Running 0 15s ← Running인데 READY는 0/1
# 서비스가 요청을 보낼 대상(엔드포인트)과 각각의 준비 상태 확인
kubectl get endpointslice -l kubernetes.io/service-name=my-service \
-o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}'
# 실제 출력:
# 10.244.1.4 ready=true
# 10.244.2.6 ready=true
# 10.244.2.7 ready=true
# 10.244.1.7 ready=false ← readiness-demo. 목록에는 있지만 요청은 안 보냄
kubectl describe pod readiness-demo | grep Unhealthy
# 실제 출력:
# Warning Unhealthy ... Readiness probe failed: HTTP probe failed with statuscode: 404
# ready.html을 만들어 "준비 완료" 상태로 만듦
kubectl exec readiness-demo -- sh -c 'echo ok > /usr/share/nginx/html/ready.html'
kubectl get pod readiness-demo
# 실제 출력 (몇 초 후):
# NAME READY STATUS RESTARTS AGE
# readiness-demo 1/1 Running 0 25s
# 다시 엔드포인트 확인 → 10.244.1.7 ready=true 로 바뀜
- 결과 읽기
STATUS Running과READY 0/1은 다른 이야기임. 컨테이너는 돌고 있지만(Running) 손님 받을 준비는 안 됨(0/1)- 404 응답 = 페이지 없음 → readiness 실패 →
ready=false→ 서비스가 이 파드로 요청을 보내지 않음 - liveness와 달리 재시작하지 않음(RESTARTS 0). 준비되면 자동으로 다시 요청을 받기 시작함
- 실무 예: 앱이 켜진 직후 DB 연결·캐시 로딩이 끝나기 전에는 readiness를 실패시켜 에러 응답을 막음
- kubelet: 각 노드에서 동작하며 파드를 실행·감시하는 에이전트 프로그램
- 프로브 (Probe): kubelet이 컨테이너 상태를 진단하는 헬스 체크. exec·TCP·HTTP(·gRPC) 방식이 있음
- livenessProbe: 실패하면 컨테이너를 재시작하는 프로브
- readinessProbe: 실패하면 서비스 전송 대상에서 빼는 프로브
- 엔드포인트슬라이스 (EndpointSlice): 서비스가 요청을 보낼 파드 IP 목록과 준비 상태를 담은 객체
- RESTARTS: 컨테이너가 재시작된 횟수. 계속 늘어나면 문제가 있다는 신호
- 원서는 "재시작"과 "요청 전송 중지"를 한 문장에 묶었지만, 실습에서 본 것처럼 liveness / readiness가 따로 담당함
- 판단 방법에 gRPC 방식이 추가됨 (v1.27 GA)
5.8. 스케일링: 시스템의 성능을 향상시키는 방법
5.8.1. 스케일링이란
- 스케일링(Scaling): 컨테이너 기술에서 요청 규모에 따라 컨테이너를 증감하는 것
- 시스템마다 요청 빈도는 다르고, 어떤 이유로 갑자기 요청이 집중되는 상황도 있음 (예: 수강 신청, 티켓 예매, 쇼핑몰 할인 이벤트)
- 처리 능력을 초과하는 요청이 들어오면 시스템 다운 등 원하지 않는 상황이 발생할 수 있음
- 스케일링의 효과
| 상황 | 동작 | 효과 |
|---|---|---|
| 요청 집중 | 컨테이너 수 증가 | 처리 능력을 요청 규모에 맞춰 시스템을 정상 유지 |
| 야간 등 요청 감소 | 컨테이너 수 감소 | 리소스 소비량을 억제해 비용 절약 |
5.8.2. 스케일링하는 방법
- 요청 건수 등의 데이터를 정기적으로 취득하고, 그 값이 일정한 임곗값보다 높거나 낮으면 리소스를 증감함
- 스케일링 여부를 결정하는 데 사용하는 데이터를 메트릭(Metric) 이라고 함
- 현재 데이터뿐 아니라 과거 데이터를 참조해, 연속으로 임곗값을 상회·하회할 때만 증감하기도 함
- 이유: 순간적인 튐에 반응해 늘렸다 줄였다를 반복하면(플래핑, flapping) 오히려 불안정해짐
- 리소스를 증감하는 두 가지 방법
| 구분 | 수평 스케일링 (Scale Out/In) | 수직 스케일링 (Scale Up/Down) |
|---|---|---|
| 방법 | 리소스의 수를 늘리거나 줄임 | 리소스의 성능을 높이거나 낮춤 |
| 예시 | 파드 3개 → 5개 | 파드 CPU 0.5코어 → 1코어 |
| 비유 | 계산대 직원을 더 뽑음 | 직원 한 명을 더 빠른 사람으로 바꿈 |
| 리소스와 처리 능력의 관계 | 선형에 가까움 (2배 → 약 2배) | 선형에서 벗어나기 쉬움 |
| 한계 | 거의 없음 (노드만 있으면 계속 추가) | 한 노드의 성능이 상한 |
수평 스케일링 vs 수직 스케일링:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[수평 스케일링: 개수를 늘림] [수직 스케일링: 크기를 키움]
[파드] ──→ [파드] [파드] [파드] [파드] ──→ [ 파 드 ]
(CPU·메모리 ↑)
쿠버네티스: HPA 쿠버네티스: VPA
클러스터 자체: CA
- 메트릭 (Metric): 스케일링 여부를 판단하는 데 사용하는 측정 데이터 (요청 수, CPU 사용률 등)
- 임곗값 (Threshold): 동작을 일으키는 기준이 되는 값
- 수평 스케일링: 리소스 수를 증감하는 방식 (스케일 아웃/인)
- 수직 스케일링: 리소스 성능을 높이거나 낮추는 방식 (스케일 업/다운)
5.8.3. 쿠버네티스에서 파드의 수평 스케일링
- 가장 단순한 수평 스케일링은 사람이 직접 파드 수를 바꾸는 것
# 파드 수를 3개 → 5개로
kubectl scale deployment my-deployment --replicas=5
kubectl get deploy my-deployment
# 실제 출력:
# NAME READY UP-TO-DATE AVAILABLE AGE
# my-deployment 5/5 5 5 2m13s
# 다시 3개로
kubectl scale deployment my-deployment --replicas=3
# my-deployment 3/3 3 3 2m16s
- 이것을 메트릭을 보고 자동으로 해 주는 것이 HPA(Horizontal Pod Autoscaler)
- HPA 컨트롤러의 동작
- 주기적으로(기본 15초) 파드의 메트릭을 취득
- 메트릭을 기반으로 필요한 파드 수를 계산
- 레플리카셋·디플로이먼트의 파드 수를 변경
- 메트릭은 기본적으로 CPU 사용률을 사용하지만, 필요하면 다른 지표를 쓰는 커스텀 메트릭도 사용할 수 있음
HPA의 필요 파드 수 계산식:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
현재 메트릭 값
필요한 파드 수 = ⌈ 현재 파드 수 × ─────────────── ⌉ (⌈ ⌉ = 올림)
목표 메트릭 값
예) 목표 CPU 50%, 현재 평균 CPU 100%, 파드 2개
→ 2 × (100 / 50) = 4개로 늘림
예) 목표 CPU 50%, 현재 평균 CPU 20%, 파드 4개
→ 4 × (20 / 50) = 1.6 → 올림해서 2개로 줄임
HPA의 동작 흐름:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[파드들] ──CPU 사용량──→ [metrics-server] ──→ ┌──────────────┐
(메트릭 수집기) │ HPA 컨트롤러 │
└──────┬───────┘
│ 필요한 파드 수 계산
▼
┌──────────────────────────┐
│ 디플로이먼트 / 레플리카셋 │ replicas 변경
└──────────────────────────┘
# hpa.yaml - my-deployment의 파드를 CPU 사용률 기준으로 2~10개 사이에서 조절
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-hpa
spec:
scaleTargetRef: # 스케일링 대상
apiVersion: apps/v1
kind: Deployment
name: my-deployment
minReplicas: 2 # 최소 파드 수
maxReplicas: 10 # 최대 파드 수
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50 # 평균 CPU 사용률 50% 유지가 목표
<unknown>이라면kubectl apply -f hpa.yaml
kubectl get hpa
# 실제 출력:
# NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
# my-hpa Deployment/my-deployment cpu: <unknown>/50% 2 10 3 20s
kubectl describe hpa my-hpa
# 실제 출력 (Conditions 부분):
# AbleToScale True SucceededGetScale the HPA controller was able to get the target's current scale
# ScalingActive False FailedGetResourceMetric ... unable to fetch metrics from resource metrics API:
# the server could not find the requested resource (get pods.metrics.k8s.io)
kubectl top pods
# 실제 출력:
# error: Metrics API not available
- 결과 읽기
- HPA는 스스로 CPU를 재지 않음. 파드의 CPU 사용량을 모아 주는 metrics-server라는 별도 구성 요소가 있어야 함
- Docker Desktop 쿠버네티스에는 metrics-server가 기본 설치되어 있지 않음 →
<unknown>,ScalingActive False AbleToScale True: 대상(디플로이먼트)은 찾았다는 뜻. 즉 HPA 설정은 맞고 메트릭만 없는 상태- 또한 CPU 사용률(%) 을 계산하려면 파드에 CPU request(5.8.4)가 지정되어 있어야 함 (사용률 = 실제 사용량 ÷ request)
- EKS·GKE·AKS 같은 관리형 쿠버네티스는 metrics-server가 기본 포함되거나 애드온으로 쉽게 켤 수 있음
5.8.4. 쿠버네티스에서 파드의 수직 스케일링
- VPA(Vertical Pod Autoscaler) 로 파드를 수직 스케일링할 수 있음
- 파드에 할당하는 리소스의 양은 두 파라미터로 정해짐
| 파라미터 | 의미 | 비유 (식당) |
|---|---|---|
| request | 리소스의 하한선 (최소 이만큼은 확보) | 예약한 좌석 수 — 반드시 비워 둠 |
| limit | 리소스의 상한선 (이 이상은 사용 불가) | 최대 수용 인원 — 넘으면 입장 불가 |
- 리소스 단위
- CPU:
1= 1코어,250m= 0.25코어 (m = 밀리코어, 1000m = 1코어) - 메모리:
128Mi= 128 메비바이트 (Mi,Gi는 1024 단위)
- CPU:
- 넘었을 때의 차이: CPU는 limit을 넘으면 느려질 뿐(제한됨), 메모리는 limit을 넘으면 컨테이너가 강제 종료(OOMKilled)됨
- VPA는 리소스 소비량에 따라 request와 limit을 자동으로 조정해 파드를 수직 스케일링함
- 원서 기준: 수평 스케일링(HPA)은 쿠버네티스에 표준 탑재되어 있지만, 수직 스케일링(VPA)은 베타 버전이며 깃허브에서 내려받아 설치해야 함
# resources.yaml
apiVersion: v1
kind: Pod
metadata:
name: resource-demo
spec:
containers:
- name: nginx
image: nginx:stable
resources:
requests: # 하한선: 스케줄러가 이만큼 여유 있는 노드에 배치
cpu: 250m # 0.25코어
memory: 128Mi
limits: # 상한선: 이 이상 사용 불가
cpu: 500m # 0.5코어
memory: 256Mi
kubectl apply -f resources.yaml
# QoS 클래스 확인 (아래 표 참고)
kubectl get pod resource-demo -o jsonpath='{.status.qosClass}'
# 실제 출력: Burstable
# 파드가 올라간 노드의 리소스 예약 현황
kubectl describe node desktop-worker
실제 출력 (일부):
Allocatable: ← 이 노드에서 파드에 줄 수 있는 총량
cpu: 12
memory: 7658696Ki
pods: 110
Allocated resources: ← 파드들의 request/limit 합계
Resource Requests Limits
-------- -------- ------
cpu 350m (2%) 600m (5%)
memory 178Mi (2%) 306Mi (4%)
- 결과 읽기
Requests cpu 350m (2%): 이 노드에 올라간 파드들이 예약한 CPU 합계. 12코어 중 0.35코어- 스케줄러는 실제 사용량이 아니라 이 request 합계를 보고 "자리가 있나"를 판단함
- request/limit 지정 방식에 따라 QoS 클래스가 정해지고, 노드 메모리가 부족할 때 누가 먼저 쫓겨날지가 결정됨
| QoS 클래스 | 조건 | 노드 자원 부족 시 | 실습에서 |
|---|---|---|---|
| Guaranteed | 모든 컨테이너가 request = limit | 가장 마지막에 종료 | - |
| Burstable | request만 있거나 request < limit | 중간 | resource-demo |
| BestEffort | request·limit 모두 없음 | 가장 먼저 종료 | 5.9.4의 my-pod |
예전에는 request/limit을 바꾸려면 파드를 새로 만들어야 했음. 최신 버전에서는 실행 중인 파드의 리소스를 그대로 바꿀 수 있음
# 변경 전 파드의 고유 ID(UID) 확인
kubectl get pod resource-demo -o jsonpath='{.metadata.uid}'
# 실제 출력: fd88b73a-de2f-4ff4-8a02-2a90bde88a64
# CPU request 250m → 400m, limit 500m → 800m 으로 변경
kubectl patch pod resource-demo --subresource resize \
--patch '{"spec":{"containers":[{"name":"nginx","resources":{"requests":{"cpu":"400m"},"limits":{"cpu":"800m"}}}]}}'
# 실제 출력: pod/resource-demo patched
kubectl get pod resource-demo
# 실제 출력:
# NAME READY STATUS RESTARTS AGE
# resource-demo 1/1 Running 0 13s ← RESTARTS 0
kubectl get pod resource-demo -o jsonpath='{.metadata.uid}{"\n"}{.status.containerStatuses[0].resources}'
# 실제 출력:
# fd88b73a-de2f-4ff4-8a02-2a90bde88a64 ← UID 그대로 = 같은 파드
# {"limits":{"cpu":"800m","memory":"256Mi"},"requests":{"cpu":"400m","memory":"128Mi"}}
- 결과 읽기: UID가 같고 RESTARTS가 0 → 같은 파드가 멈추지 않고 CPU만 늘어남. VPA가 이 기능을 쓰면 수직 스케일링 때 서비스 중단을 줄일 수 있음
- 매니페스트에서는
requests/limits(복수형)로 씀. request는 엄밀히 "하한선"이라기보다 스케줄러가 노드를 고를 때 기준으로 삼는 예약량임 - VPA는 지금도 쿠버네티스 본체가 아닌 별도 애드온(kubernetes/autoscaler 저장소)으로 설치함
- In-place Pod Resize는 v1.35에서 GA가 되었고, 위 실습처럼 v1.36 클러스터에서 기본으로 동작함
5.8.5. 클러스터 자체의 수평 스케일링
- 클러스터가 AWS, 애저 같은 퍼블릭 클라우드에 구축된 경우 CA(Cluster Autoscaler) 로 클러스터 자체를 수평 스케일링할 수 있음
- CA는 클러스터를 구성하는 시스템(노드)의 수를 늘리거나 줄임
- HPA가 파드를 아무리 늘려도 노드(서버) 자리가 없으면 파드를 띄울 수 없음 → 그때 노드를 늘리는 것이 CA
- HPA와 증가 타이밍이 다름
- HPA: 메트릭이 임곗값을 넘으면 파드를 늘림
- CA: 리소스 부족으로 파드를 시작하지 못한 경우(Pending) 노드를 늘림
- 노드를 줄이는 기준: 한 노드의 리소스 사용률이 낮은 상태가 일정 시간 계속되면 해당 노드를 폐기 (그 노드의 파드는 다른 노드로 옮겨짐)
CPU를 100코어 요청하는 파드를 만들어 봄 (노드 한 대는 12코어)
# too-big.yaml
apiVersion: v1
kind: Pod
metadata:
name: too-big
spec:
containers:
- name: nginx
image: nginx:stable
resources:
requests:
cpu: "100" # 100코어 요청
kubectl apply -f too-big.yaml
kubectl get pod too-big
# 실제 출력:
# NAME READY STATUS RESTARTS AGE
# too-big 0/1 Pending 0 4s ← 어느 노드에도 배치되지 못함
kubectl describe pod too-big
# 실제 출력 (Events):
# Warning FailedScheduling 4s default-scheduler 0/3 nodes are available:
# 1 node(s) had untolerated taint(s), 2 Insufficient cpu. ...
- 결과 읽기
Pending: 파드는 만들어졌지만 올라갈 노드가 정해지지 않은 상태0/3 nodes are available: 노드 3대 중 가능한 노드가 0대1 node(s) had untolerated taint(s): 컨트롤 플레인 노드는 일반 파드를 받지 않도록 테인트(출입 금지 표시) 가 걸려 있음2 Insufficient cpu: 워커 2대는 CPU가 부족함
- 퍼블릭 클라우드에 CA가 있었다면 바로 이
Pending+Insufficient cpu를 보고 노드를 추가함. 로컬 Docker Desktop에는 CA가 없으므로 계속 Pending으로 남음
CA의 리소스 사용률 계산식:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
요청된(request) CPU 및 메모리 양
리소스 사용률 = ─────────────────────────────────────
사용 가능한(allocatable) CPU 및 메모리 양
→ 일정 임곗값(기본 50%)을 밑도는 상태가 지속(기본 10분)되면 노드 축소
※ 실제 사용량이 아니라 request 합계 기준 → 실습 7의 "Allocated resources"가 바로 이 값
HPA와 CA의 연계:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
요청 급증
│
▼
[HPA] 파드 4개 → 8개로 증가 지시
│
▼
노드 리소스 부족 → 새 파드 4개가 Pending (실습 9의 상태)
│
▼
[CA] Pending 파드 감지 → 클라우드에 노드 추가 요청
│
▼
새 노드에 Pending 파드가 배치되어 실행
| 구분 | HPA | VPA | CA |
|---|---|---|---|
| 대상 | 파드 | 파드 | 노드(클러스터) |
| 방향 | 수평 (개수) | 수직 (크기) | 수평 (개수) |
| 조정 항목 | 레플리카 수 | request / limit | 노드 수 |
| 증가 계기 | 메트릭이 임곗값 초과 | 리소스 소비량 | 리소스 부족으로 파드 시작 불가 (Pending) |
| 필요한 것 | metrics-server | VPA 애드온 + metrics-server | 퍼블릭 클라우드 + CA 설치 |
- HPA (Horizontal Pod Autoscaler): 메트릭에 따라 파드 수를 자동 증감하는 기능
- VPA (Vertical Pod Autoscaler): 리소스 소비량에 따라 파드의 request/limit을 자동 조정하는 기능
- CA (Cluster Autoscaler): 퍼블릭 클라우드에서 클러스터의 노드 수를 자동 증감하는 기능
- metrics-server: 파드·노드의 CPU/메모리 사용량을 모아 HPA·
kubectl top에 제공하는 구성 요소 - request / limit: 파드에 할당하는 리소스의 하한선(예약량) / 상한선
- QoS 클래스: request/limit 지정 방식에 따른 파드 등급. 자원 부족 시 종료 우선순위를 결정
- Pending: 파드가 생성됐지만 배치될 노드가 정해지지 않은 상태
- 테인트 (Taint): 노드에 거는 "이 조건이 없는 파드는 받지 않음" 표시
- AWS(EKS)에서는 CA 대신 Karpenter도 널리 쓰임. 노드 그룹 단위가 아니라 Pending 파드에 맞는 인스턴스 타입을 직접 골라 노드를 띄우므로 더 빠르고 유연함 (v1.0 GA)
5.9. 쿠버네티스의 주요 명령어: 필수 명령어 소개
5.9.1. 선언형 기술로 클러스터 조작
- 쿠버네티스는 매니페스트(Manifest) 라는 텍스트 파일로 클러스터를 조작함
- 매니페스트는 YAML 또는 JSON 형식의 선언형으로 작성함
- 선언형과 절차형의 차이
| 구분 | 선언형 기술 | 절차형 기술 |
|---|---|---|
| 작성 내용 | 기대하는 상태 | 기대하는 상태로 바꾸기 위한 조작 순서 |
| 예시 | "파드 이름은 my-pod, 이미지는 nginx:stable" | "① 이미지를 받고 ② 컨테이너를 만들고 ③ 시작한다" |
| 비유 | 택시에서 "강남역이요" (목적지) | "직진 후 두 번째 신호에서 우회전..." (경로) |
| 쿠버네티스 | ○ 사용 | ✗ 불필요 |
- 쿠버네티스는 현재 클러스터 상태와 매니페스트를 비교해 조작 절차를 자동으로 계획하므로 절차형 작성이 필요하지 않음 (→ 5-1의 컨트롤러 개념과 같은 원리)
- 선언형의 장점: 같은 매니페스트를 몇 번 적용해도 결과가 같음 (실습 10에서
unchanged확인)
YAML 매니페스트의 기본 구조 (모든 매니페스트 공통):
apiVersion: v1 # ① 이 객체를 다루는 API 버전 (종류마다 정해져 있음)
kind: Pod # ② 만들 객체의 종류 (Pod, Deployment, Service ...)
metadata: # ③ 객체를 식별하는 정보
name: my-pod # 이름 (같은 종류·네임스페이스 안에서 유일)
labels: # 레이블 (5.9.2)
app: my-app
spec: # ④ 기대하는 상태 (종류마다 내용이 다름)
containers:
- name: nginx # "-"는 목록의 한 항목 (컨테이너가 여러 개일 수 있음)
image: nginx:stable
- 들여쓰기는 스페이스로만 (탭 문자 사용 불가). 들여쓰기 깊이가 곧 구조임
key: value에서 콜론 뒤에 공백 한 칸 필수- 목록 항목은
-(하이픈 + 공백)으로 시작
5.9.2. 매니페스트에서 객체를 선택하는 방법
- 레이블(Label): 객체(파드, 디플로이먼트 등)에 할당된 키와 값의 쌍
- 하나의 객체에 레이블을 여러 개 할당할 수 있음
- 레이블 셀렉터(Label Selector): 객체를 선택하기 위해 지정하는 레이블의 조건
- 레이블과 레이블 셀렉터를 쓰면 대상 객체가 여러 개여도 일괄적으로 선택할 수 있음
kubectl명령어에서도 사용 가능- 레이블은 단순한 메모가 아니라 쿠버네티스 객체들을 서로 연결하는 접착제임
- 레플리카셋은 레이블로 "내가 관리할 파드"를 찾고
- 서비스는 레이블로 "요청을 보낼 파드"를 찾음
레이블과 레이블 셀렉터:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
셀렉터: app = my-app
│
│ 조건에 맞는 객체만 선택
▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 파드 1 │ │ 파드 2 │ │ 파드 3 │
│ app: my-app ✔ │ │ app: my-app ✔ │ │ app: demo ✗ │
│ │ │ │ │ tier: backend │
└─────────────────┘ └─────────────────┘ └─────────────────┘
# 이미 있는 파드에 레이블 2개 붙이기
kubectl label pod resource-demo app=demo tier=backend
# 실제 출력: pod/resource-demo labeled
# 모든 파드와 레이블 표시
kubectl get pods --show-labels
# 실제 출력:
# NAME READY STATUS RESTARTS AGE LABELS
# client 1/1 Running 0 4m29s run=client
# my-deployment-7f7c74cb79-9xsl5 1/1 Running 0 44s app=my-app,pod-template-hash=7f7c74cb79
# my-deployment-7f7c74cb79-pg5j9 1/1 Running 0 48s app=my-app,pod-template-hash=7f7c74cb79
# my-deployment-7f7c74cb79-ppsgj 1/1 Running 0 45s app=my-app,pod-template-hash=7f7c74cb79
# resource-demo 1/1 Running 0 19s app=demo,tier=backend
# -l (--selector): 레이블 셀렉터로 골라내기
kubectl get pods -l app=my-app
# 실제 출력: my-deployment-... 3개만 표시
# 여러 조건: 쉼표(,) = AND, in (...) = 목록 중 하나
kubectl get pods -l 'app in (demo),tier=backend'
# 실제 출력: resource-demo 1개만 표시
- 결과 읽기
run=client:kubectl run으로 만든 파드에는 자동으로run=<이름>레이블이 붙음pod-template-hash=...: 디플로이먼트가 버전 구분용으로 자동으로 붙이는 레이블 (5.9.6, 5.10.4에서 다시 등장)
- 매니페스트 (Manifest): 클러스터에 기대하는 상태를 YAML/JSON으로 선언한 텍스트 파일
- 선언형 기술: 기대하는 상태를 적는 방식. 절차는 쿠버네티스가 자동으로 계획
- 절차형 기술: 원하는 상태가 되기 위한 조작 순서를 적는 방식
- 레이블 (Label): 객체에 붙이는 키-값 쌍. 하나의 객체에 여러 개 할당 가능
- 레이블 셀렉터 (Label Selector): 레이블 조건으로 객체를 선택하는 방법
5.9.3. 쿠버네티스 명령어를 사용할 수 있는 환경
| 환경 | 설치 방법 |
|---|---|
| 리눅스 (CentOS, 우분투 등) | yum, apt 같은 패키지 관리 시스템으로 설치 |
| 윈도 / 맥OS | 도커 데스크톱(2018년 7월 이후 정식 버전)에 포함 → 설정 화면에서 쿠버네티스 활성화 |
- 도커 데스크톱에서 활성화하는 방법: Settings → Kubernetes → Enable Kubernetes 체크 → Apply & restart. 왼쪽 아래에 쿠버네티스 상태가 초록색으로 표시되면 준비 완료
- 도커 명령어와 마찬가지로 명령줄 인터페이스(CLI) 로 조작함
- 리눅스·맥OS: 터미널
- 윈도: 명령 프롬프트, 파워셸 (이 노트의 실습은 Git Bash에서 실행)
# 설치가 잘 됐는지 확인하는 첫 명령어
kubectl version
# 실제 출력:
# Client Version: v1.34.0 ← 내 PC의 kubectl
# Kustomize Version: v5.7.1
# Server Version: v1.36.1 ← 클러스터(API 서버)
kubectl cluster-info
# 실제 출력:
# Kubernetes control plane is running at https://127.0.0.1:62824
# CoreDNS is running at https://127.0.0.1:62824/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Client와Server가 둘 다 나오면 kubectl이 클러스터와 통신에 성공한 것
- CentOS Linux는 지원이 종료(CentOS 7: 2024년 6월)되어 Rocky Linux, AlmaLinux 등이 대안
- 리눅스 패키지 저장소는 예전
apt.kubernetes.io/yum.kubernetes.io가 동결되고pkgs.k8s.io로 바뀜 - 로컬 학습용으로는 도커 데스크톱 외에 minikube, kind 도 많이 사용함. 최신 도커 데스크톱은 kind 방식으로 멀티 노드 클러스터도 만들 수 있음 (이 노트의 실습 환경)
5.9.4. 파드의 생성/확인/명령어 실행/변경/삭제
- 매니페스트
pod.yaml로my-pod라는 파드를 만듦- 레이블: 키
app, 값my-app - 컨테이너 이미지:
nginx:stable
- 레이블: 키
# pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: my-pod # 파드 이름
labels:
app: my-app # 레이블 (키: app, 값: my-app)
spec:
containers:
- name: nginx
image: nginx:stable # 실행할 컨테이너 이미지
① 생성
kubectl apply -f pod.yaml
# 실제 출력: pod/my-pod created
kubectl get pods # 바로 확인
# 실제 출력:
# NAME READY STATUS RESTARTS AGE
# my-pod 0/1 ContainerCreating 0 0s
kubectl get pods # 15초 후 다시 확인
# 실제 출력:
# NAME READY STATUS RESTARTS AGE
# my-pod 1/1 Running 0 15s
kubectl get pods 출력 읽는 법:
| 열 | 의미 | 예시 해석 |
|---|---|---|
| NAME | 파드 이름 | my-pod |
| READY | 준비된 컨테이너 수 / 전체 컨테이너 수 | 1/1 = 1개 중 1개 준비 완료 |
| STATUS | 파드 상태 | ContainerCreating(이미지 받는 중) → Running(실행 중) |
| RESTARTS | 컨테이너 재시작 횟수 | 0 = 한 번도 죽지 않음 |
| AGE | 만들어진 지 지난 시간 | 15s |
- 처음에
ContainerCreating인 이유: 노드에nginx:stable이미지가 없어서 레지스트리에서 내려받는 중이기 때문
② 자세히 확인 (-o wide, describe)
kubectl get pods -o wide --show-labels
# 실제 출력:
# NAME READY STATUS RESTARTS AGE IP NODE ... LABELS
# my-pod 1/1 Running 0 15s 10.244.2.2 desktop-worker ... app=my-app
kubectl describe pod my-pod # 파드의 모든 정보 + 이벤트 기록
실제 출력 (맨 아래 Events 부분):
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 30s default-scheduler Successfully assigned default/my-pod to desktop-worker
Normal Pulling 29s kubelet Pulling image "nginx:stable"
Normal Pulled 16s kubelet Successfully pulled image "nginx:stable" in 13.073s ... Image size: 63608941 bytes.
Normal Created 15s kubelet Container created
Normal Started 15s kubelet Container started
- 결과 읽기: 파드 하나가 뜨기까지의 과정이 그대로 기록됨
default-scheduler가desktop-worker노드를 골라 배정 (Scheduled)- 그 노드의
kubelet이 이미지를 받음 (Pulling → Pulled, 약 13초, 약 63MB) - 컨테이너를 만들고 시작 (Created → Started)
- 이 과정에 등장하는 scheduler·kubelet이 5.12절의 구성 요소임
- 파드가 안 뜰 때 가장 먼저 볼 곳이
describe의 Events임 (실습 9의 FailedScheduling도 여기서 확인)
③ 파드 안에서 명령 실행 (exec)
kubectl exec -it my-pod -- echo Hello Kubernetes
# 실제 출력: Hello Kubernetes
kubectl exec my-pod -- nginx -v # 컨테이너 안의 nginx 버전 확인
# 실제 출력: nginx version: nginx/1.30.6
kubectl exec my-pod -- hostname # 컨테이너의 호스트 이름 = 파드 이름
# 실제 출력: my-pod
--: 여기까지가 kubectl의 옵션이고, 뒤는 컨테이너 안에서 실행할 명령이라는 구분선-it:-i(입력 연결) +-t(터미널 할당).kubectl exec -it my-pod -- sh처럼 컨테이너 안에 들어가 셸을 쓸 때 필요함. 위처럼 명령 한 줄만 실행할 때는 없어도 됨
Unable to use a TTY 경고
스크립트나 일부 터미널에서 -it를 쓰면 Unable to use a TTY - input is not a terminal ... 경고가 나올 수 있음. 명령 결과에는 영향 없음. 한 줄 명령이면 -it를 빼면 됨
④ 변경
# pod.yaml을 고치지 않고 다시 적용
kubectl apply -f pod.yaml
# 실제 출력: pod/my-pod unchanged ← 기대 상태와 같으니 아무것도 안 함 (선언형)
# 이미지를 nginx:stable → nginx:stable-alpine 으로 바꾼 매니페스트 적용
kubectl apply -f pod-v2.yaml
# 실제 출력: pod/my-pod configured
kubectl get pods
# 실제 출력:
# NAME READY STATUS RESTARTS AGE
# my-pod 1/1 Running 1 (15s ago) 42s ← 컨테이너가 새 이미지로 재시작됨
kubectl apply의 출력 단어: created(새로 만듦) / configured(바꿈) / unchanged(그대로)
실행 중인 파드에 포트(ports)를 추가해 다시 적용하면 오류가 남
The Pod "my-pod" is invalid: spec: Forbidden: pod updates may not change fields other than
`spec.containers[*].image`, `spec.initContainers[*].image`, `spec.activeDeadlineSeconds`,
`spec.tolerations` (only additions to existing tolerations), `spec.terminationGracePeriodSeconds` ...
→ 파드는 이미지 등 일부 항목만 바꿀 수 있음. 그 밖의 변경은 파드를 지우고 새로 만들어야 함. 그래서 실무에서는 파드를 직접 만들지 않고 디플로이먼트(5.9.6)가 파드를 새로 만들어 교체하게 함
⑤ 삭제
kubectl delete -f pod.yaml
# 출력: pod "my-pod" deleted
5.9.5. 레플리카셋의 생성/확인/변경/삭제
- 매니페스트
replicaset.yaml로my-replicaset이라는 레플리카셋을 만듦template의 내용이 파드를 만들 때와 비슷함 → template은 "파드를 찍어 내는 틀"- 파드 3개 포함, 각 파드는 레이블
app: my-app, 이미지nginx:stable
# replicaset.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: my-replicaset
spec:
replicas: 3 # 유지할 파드 수
selector:
matchLabels:
app: my-app # 이 레이블을 가진 파드를 "내 것"으로 관리 (레이블 셀렉터)
template: # ↓ 여기부터 파드 매니페스트와 거의 같음 (파드를 찍어 내는 틀)
metadata:
labels:
app: my-app # 틀로 만든 파드에 붙일 레이블 (selector와 일치해야 함)
spec:
containers:
- name: nginx
image: nginx:stable
실습 10의 my-pod(레이블 app=my-app)를 지우지 않은 상태에서 레플리카셋을 만들어 봄
kubectl apply -f replicaset.yaml
# 실제 출력: replicaset.apps/my-replicaset created
kubectl get replicasets
# 실제 출력:
# NAME DESIRED CURRENT READY AGE
# my-replicaset 3 3 2 3s
kubectl get pods --show-labels
# 실제 출력:
# NAME READY STATUS RESTARTS AGE LABELS
# my-pod 1/1 Running 1 (24s ago) 51s app=my-app
# my-replicaset-6r4vr 0/1 ContainerCreating 0 3s app=my-app
# my-replicaset-fbqll 1/1 Running 0 3s app=my-app
- 결과 읽기:
replicas: 3인데 새 파드는 2개만 생김- 레플리카셋은 "template으로 만든 파드"가 아니라 "셀렉터(
app=my-app)에 맞는 파드" 를 셈 - 이미
app=my-app레이블을 가진my-pod가 있으니 그것을 자기 것으로 입양하고 2개만 더 만듦
- 레플리카셋은 "template으로 만든 파드"가 아니라 "셀렉터(
# 각 파드의 주인(ownerReferences) 확인
kubectl get pods -o custom-columns=NAME:.metadata.name,OWNER:.metadata.ownerReferences[0].name
# 실제 출력:
# NAME OWNER
# my-pod my-replicaset ← 따로 만든 파드인데 주인이 생김
# my-replicaset-6r4vr my-replicaset
# my-replicaset-fbqll my-replicaset
kubectl get replicasets 출력 읽는 법:
| 열 | 의미 |
|---|---|
| DESIRED | 원하는 파드 수 (replicas) |
| CURRENT | 현재 만들어진 파드 수 |
| READY | 그중 준비 완료된 파드 수 |
# 파드 하나를 일부러 삭제
kubectl delete pod my-replicaset-fbqll
kubectl get pods
# 실제 출력:
# NAME READY STATUS RESTARTS AGE
# my-pod 1/1 Running 1 (31s ago) 58s
# my-replicaset-6r4vr 1/1 Running 0 10s
# my-replicaset-qzdsv 1/1 Running 0 2s ← 2초 전에 새로 생김
- 지운
fbqll대신 새 파드qzdsv가 즉시 생김 → 레플리카셋이 "3개 유지"라는 기대 상태를 지킴 (5-1의 컨트롤러 = 에어컨 비유)
# replicas: 3 → 5 로 고친 매니페스트 적용 (변경 방법은 파드와 동일)
kubectl apply -f rs5.yaml
# 실제 출력: replicaset.apps/my-replicaset configured
kubectl get rs
# 실제 출력:
# NAME DESIRED CURRENT READY AGE
# my-replicaset 5 5 5 22s
kubectl get pods -o wide # NODE 열만 보면
# my-replicaset-6r4vr ... desktop-worker2
# my-replicaset-8b45p ... desktop-worker
# my-replicaset-qzdsv ... desktop-worker2
# my-replicaset-tkmwz ... desktop-worker
# my-pod ... desktop-worker
- 스케줄러가 파드를 두 워커 노드에 나눠 배치함 → 노드 하나가 죽어도 다른 노드의 파드가 살아 있음 (가용성)
# 삭제 (방법은 파드와 동일)
kubectl delete -f replicaset.yaml
# 실제 출력: replicaset.apps "my-replicaset" deleted from default namespace
kubectl get pods
# 실제 출력: No resources found in default namespace.
- 레플리카셋을 지우면 자기가 관리하던 파드도 함께 삭제됨. 입양된
my-pod도 같이 사라짐
5.9.6. 디플로이먼트의 생성/확인/변경/삭제
- 매니페스트
deployment.yaml로my-deployment라는 디플로이먼트를 만듦- 레플리카셋 매니페스트와 거의 동일 (
kind만 다름) - 디플로이먼트 안에 단일 레플리카셋, 레플리카셋 안에 파드 3개
- 레플리카셋 매니페스트와 거의 동일 (
# deployment.yaml
apiVersion: apps/v1
kind: Deployment # ← 레플리카셋과의 차이는 사실상 이 줄
metadata:
name: my-deployment
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: nginx
image: nginx:stable
kubectl apply -f deployment.yaml
# 실제 출력: deployment.apps/my-deployment created
kubectl rollout status deployment/my-deployment # 배포가 끝날 때까지 지켜보기
# 실제 출력:
# Waiting for deployment "my-deployment" rollout to finish: 0 of 3 updated replicas are available...
# Waiting for deployment "my-deployment" rollout to finish: 1 of 3 updated replicas are available...
# Waiting for deployment "my-deployment" rollout to finish: 2 of 3 updated replicas are available...
# deployment "my-deployment" successfully rolled out
kubectl get deployments
# 실제 출력:
# NAME READY UP-TO-DATE AVAILABLE AGE
# my-deployment 3/3 3 3 1s
kubectl get rs
# 실제 출력:
# NAME DESIRED CURRENT READY AGE
# my-deployment-5f9c57bbf7 3 3 3 1s
kubectl get pods -o wide
# 실제 출력:
# NAME READY STATUS RESTARTS AGE IP NODE
# my-deployment-5f9c57bbf7-9w5sz 1/1 Running 0 1s 10.244.1.4 desktop-worker2
# my-deployment-5f9c57bbf7-c4wvz 1/1 Running 0 1s 10.244.2.6 desktop-worker
# my-deployment-5f9c57bbf7-k5slg 1/1 Running 0 1s 10.244.2.7 desktop-worker
kubectl get deployments 출력 읽는 법:
| 열 | 의미 |
|---|---|
| READY | 준비된 파드 수 / 원하는 파드 수 |
| UP-TO-DATE | 최신 template으로 만들어진 파드 수 (레플리카셋에는 없는 열) |
| AVAILABLE | 사용자에게 서비스 가능한 파드 수 |
이름에 숨은 3단 구조:
디플로이먼트 → 레플리카셋 → 파드의 이름 규칙:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
my-deployment ← 디플로이먼트 (내가 지은 이름)
my-deployment-5f9c57bbf7 ← 레플리카셋 (+ template의 해시값)
my-deployment-5f9c57bbf7-9w5sz ← 파드 (+ 랜덤 5글자)
5f9c57bbf7 = pod-template-hash
→ template 내용(이미지 등)이 바뀌면 해시가 바뀌고 → 새 레플리카셋이 만들어짐 (5.10.4)
- 변경·삭제 방법은 파드와 동일. 단, 디플로이먼트는 파드와 달리 template의 어떤 항목이든 바꿀 수 있음 (새 파드를 만들어 교체하기 때문)
5.9.7. 서비스의 생성/확인/변경/삭제
- 매니페스트
service.yaml로my-service라는 서비스를 만듦tcp/8080으로 들어오는 접속을 파드의tcp/80으로 전달하는 규칙 → 규칙 이름은http- 전송 대상: 레이블
app: my-app이 지정된 파드
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: my-app # 이 레이블을 가진 파드로 전송
ports:
- name: http # 규칙 이름
protocol: TCP
port: 8080 # 서비스가 받는 포트 (손님이 거는 번호)
targetPort: 80 # 파드로 전달할 포트 (nginx가 듣고 있는 번호)
kubectl apply -f service.yaml
# 실제 출력: service/my-service created
kubectl get services
# 실제 출력:
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 14d
# my-service ClusterIP 10.96.103.142 <none> 8080/TCP 0s
| 열 | 의미 |
|---|---|
| TYPE | 서비스 유형. type을 안 적으면 기본값 ClusterIP (클러스터 내부 전용, 5-1의 5.5.3) |
| CLUSTER-IP | 이 서비스용으로 발행된 가상 IP 주소 |
| EXTERNAL-IP | 외부 공개 IP. ClusterIP 유형이라 <none> |
| PORT(S) | 서비스가 받는 포트 |
kubernetes서비스는 처음부터 있는 것으로, 클러스터 안에서 API 서버에 접속하기 위한 서비스임
kubectl describe service my-service
실제 출력:
Name: my-service
Namespace: default
Selector: app=my-app ← 이 레이블의 파드로 보냄
Type: ClusterIP
IP: 10.96.103.142 ← 가상 IP
Port: http 8080/TCP ← 규칙 이름과 받는 포트
TargetPort: 80/TCP ← 파드 쪽 포트
Endpoints: 10.244.1.4:80,10.244.2.6:80,10.244.2.7:80 ← 실제 파드 3개의 IP:포트
Session Affinity: None
- 결과 읽기:
Endpoints에 실습 13의 파드 IP 3개가 자동으로 들어가 있음. 레이블 셀렉터로 찾아낸 것 - 실제로 요청을 보내 나눠 받는 모습은 5.7.3의 실습 1, iptables 규칙은 실습 2 참고
- 변경·삭제 방법은 파드와 동일
주요 명령어 정리:
| 작업 | 명령어 | 비고 |
|---|---|---|
| 생성 / 변경 | kubectl apply -f <매니페스트> |
출력: created / configured / unchanged |
| 목록 확인 | kubectl get <종류> |
pods(po), replicasets(rs), deployments(deploy), services(svc) |
| 자세히 확인 | kubectl get <종류> -o wide |
IP, 노드 등 추가 열 |
| 상세 + 이벤트 | kubectl describe <종류> <이름> |
문제 해결의 출발점 |
| 레이블로 선택 | kubectl get <종류> -l <조건> |
-l app=my-app |
| 명령 실행 | kubectl exec [-it] <파드> -- <명령> |
셸 접속은 -it ... -- sh |
| 파드 수 변경 | kubectl scale deploy <이름> --replicas=N |
실습 5 |
| 배포 진행 확인 | kubectl rollout status deploy/<이름> |
실습 13 |
| 삭제 | kubectl delete -f <매니페스트> |
또는 kubectl delete <종류> <이름> |
리소스 매니페스트의 관계:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
service.yaml
selector: app=my-app ─────────────┐ 레이블로 대상 파드 선택
▼
deployment.yaml ──→ 레플리카셋 ──→ 파드 ×3 (labels: app=my-app)
template 안에 ↑
pod.yaml 내용이 들어 있음 ──────────┘
5.10. 자동 빌드: 이미지 생성부터 배포까지 절차를 자동화
5.10.1. 자동 빌드란
- 자동 빌드: 프로그램 등을 사용할 수 있는 상태로 만드는 절차를 자동화하는 것
- 프로그램을 릴리스하기까지 필요한 단계
- 컴파일: C언어, 자바 등은 프로그램을 이진 코드로 변환해야 함
- 라이브러리 설치: 외부 라이브러리에 의존하면 미리 설치해야 함
- 테스트: 예상대로 실행되는지 다양한 테스트를 진행
- 자동 빌드는 이 단계들을 자동화함
- 자동 빌드의 성과물을 아티팩트(Artifact) 라고 부르기도 함 (쿠버네티스 환경에서는 보통 컨테이너 이미지가 아티팩트)
5.10.2. 자동 배포란
- 자동 배포: 프로그램 등을 릴리스하는 절차를 자동화하는 것
- 빌드로 사용 가능한 상태가 된 프로그램을 사용자가 접근할 수 있게 릴리스하려면
- 서버에 프로그램을 업로드해야 함
- 릴리스에 따라 데이터베이스 구조 변경 같은 작업이 필요할 수도 있음
- 자동 배포는 이 단계들을 자동화함
사람이 매번 손으로 하면 순서를 빠뜨리거나(테스트 생략), 환경마다 다르게 하거나(내 PC에서는 됐는데...), 배포가 무서워서 드물게 하게 됨. 자동화하면 같은 절차가 매번 똑같이 실행되므로 작은 변경을 자주, 안전하게 내보낼 수 있음. 이 흐름을 흔히 CI/CD라고 부름
- 자동 빌드: 컴파일·라이브러리 설치·테스트 등 프로그램을 사용 가능한 상태로 만드는 절차의 자동화 (CI, Continuous Integration에 대응)
- 자동 배포: 서버 업로드·DB 변경 등 릴리스 절차의 자동화 (CD, Continuous Delivery/Deployment에 대응)
- 아티팩트 (Artifact): 자동 빌드의 결과물 (실행 파일, 컨테이너 이미지 등)
5.10.3. 이미지 생성부터 배포까지의 절차
- 쿠버네티스 기반 애플리케이션은 변경 사항을 릴리스하기 전에 3단계가 필요함
| 단계 | 작업 | 내용 | 대표 명령 |
|---|---|---|---|
| 1 | 이미지 생성 | 도커 파일(Dockerfile)로 컨테이너 이미지를 생성 | docker build |
| 2 | 레지스트리 등록 | 이미지를 레지스트리에 푸시해 운영 환경에서 가져올 준비 | docker push |
| 3 | 배포 업데이트 | 디플로이먼트 매니페스트의 이미지 버전을 변경하고 kubectl로 적용 → 이전 파드 중지 + 새 파드 시작 |
kubectl apply |
- 자동화하려면 절차를 시작하는 트리거를 설정하고, 트리거에 해당하는 이벤트가 발생하면 빌드용 서버에서 절차가 수행되도록 함
- 예: 트리거 = "git 저장소로 프로그램 푸시"
이미지 생성부터 배포까지 자동화:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[개발자]
│ git push (트리거)
▼
[빌드용 서버] ──────────────────────────────────────────┐
│ │
│ 1. 이미지 생성 │
│ docker build -t my-app:v2 . │
▼ │
│ 2. 레지스트리 등록 │ 자동화
│ docker push registry.example.com/my-app:v2 │
▼ │
│ 3. 배포 업데이트 │
│ 매니페스트 image: my-app:v1 → v2 │
│ kubectl apply -f deployment.yaml │
▼ ─────────────────────────────────────────────────┘
[쿠버네티스 클러스터]
└─ 이전 파드(v1) 중지 + 새 파드(v2) 시작 ← 이 부분을 5.10.4에서 실습
5.10.4. 새로운 파드로 변경하는 방법
- 배포 업데이트는 기존 파드를 새로운 파드로 바꾸는 작업
- 파드를 바꾸는 방법을 업데이트 전략이라고 하며, 디플로이먼트 설정에서 지정함
| 구분 | 재생성 (Recreate) | 롤링 업데이트 (RollingUpdate) |
|---|---|---|
| 동작 | 기존 파드를 모두 중지한 후 새 파드 시작 | 기존 파드를 줄이는 동시에 새 파드를 늘림 |
| 다운타임 | 일시적으로 모든 파드가 다운 | 모든 파드가 다운되지는 않음 |
| 버전 혼재 | 기존·새 파드가 섞이지 않음 | 일시적으로 기존·새 파드가 섞임 |
| 기본값 | - | ○ (기본 적용) |
| 언제 쓰나 | 두 버전이 동시에 돌면 안 될 때 (DB 구조가 바뀌는 경우 등) | 대부분의 웹 서비스 |
업데이트 전략 비교 (파드 3개):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[재생성]
v1 v1 v1 → (전부 중지: 서비스 중단) → v2 v2 v2
[롤링 업데이트]
v1 v1 v1 → v1 v1 v2 → v1 v2 v2 → v2 v2 v2
└──── 서비스는 계속, 버전이 섞인 구간 ────┘
실습 13의 my-deployment(nginx:stable, 파드 3개)의 이미지를 nginx:stable-alpine으로 바꾸고, 레플리카셋 2개의 파드 수 변화를 1초마다 찍어 봄
# 이미지 변경 (매니페스트를 고쳐 apply 하는 것과 같은 효과)
kubectl set image deployment/my-deployment nginx=nginx:stable-alpine
# 실제 출력: deployment.apps/my-deployment image updated
kubectl get rs -l app=my-app # 1초 간격으로 반복 실행
실제 출력 (DESIRED / CURRENT / READY):
기존 RS (5f9c57bbf7) 새 RS (6bf9d78567)
t+0s 3 / 3 / 3 1 / 1 / 0 ← 새 파드 1개 생성 시작
t+1s 2 / 2 / 2 2 / 2 / 1 ← 기존 1개 줄이고 새 것 1개 더
t+4s 1 / 1 / 1 3 / 3 / 2
t+5s 0 / 0 / 0 3 / 3 / 3 ← 교체 완료
kubectl get rs -l app=my-app
# 실제 출력 (완료 후):
# NAME DESIRED CURRENT READY AGE
# my-deployment-5f9c57bbf7 0 0 0 2m54s ← 지워지지 않고 0개로 남음
# my-deployment-6bf9d78567 3 3 3 10s
kubectl describe deployment my-deployment
실제 출력 (일부):
StrategyType: RollingUpdate
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Events:
Reason From Message
ScalingReplicaSet deployment-controller Scaled up replica set my-deployment-6bf9d78567 from 0 to 1
ScalingReplicaSet deployment-controller Scaled down replica set my-deployment-5f9c57bbf7 from 3 to 2
ScalingReplicaSet deployment-controller Scaled up replica set my-deployment-6bf9d78567 from 1 to 2
ScalingReplicaSet deployment-controller Scaled down replica set my-deployment-5f9c57bbf7 from 2 to 1
ScalingReplicaSet deployment-controller Scaled up replica set my-deployment-6bf9d78567 from 2 to 3
ScalingReplicaSet deployment-controller Scaled down replica set my-deployment-5f9c57bbf7 from 1 to 0
- 결과 읽기
- 이미지(template)가 바뀌자 해시가 다른 새 레플리카셋(
6bf9d78567)이 생김 (5.9.6의 이름 규칙) deployment-controller가 새 RS +1 → 기존 RS -1을 번갈아 반복 → 책의 "기존 파드를 줄이는 동시에 새 파드를 늘림"이 이것25% max unavailable, 25% max surge: 기본 설정. 파드 3개 기준- maxSurge 25% → 3 × 0.25 = 0.75 → 올림 1: 업데이트 중 최대 3 + 1 = 4개까지 허용
- maxUnavailable 25% → 0.75 → 내림 0: 준비된 파드가 3개 아래로 내려가지 않게 함
- 기존 RS가 0개로 남아 있는 이유: 되돌리기(롤백)용
- 이미지(template)가 바뀌자 해시가 다른 새 레플리카셋(
kubectl rollout history deployment/my-deployment
# 실제 출력:
# REVISION CHANGE-CAUSE
# 1 <none> ← nginx:stable
# 2 <none> ← nginx:stable-alpine
kubectl rollout undo deployment/my-deployment
# 실제 출력: deployment.apps/my-deployment rolled back
kubectl get rs -l app=my-app
# 실제 출력:
# NAME DESIRED CURRENT READY AGE
# my-deployment-5f9c57bbf7 3 3 3 3m2s ← 남겨 둔 기존 RS가 다시 3개로
# my-deployment-6bf9d78567 0 0 0 18s
- 남겨 둔 레플리카셋을 다시 늘리는 것만으로 빠르게 되돌릴 수 있음. 이것도 롤링 업데이트 방식으로 진행됨
# 업데이트 전략을 Recreate로 변경
kubectl patch deployment my-deployment -p '{"spec":{"strategy":{"type":"Recreate","rollingUpdate":null}}}'
kubectl set image deployment/my-deployment nginx=nginx:stable-alpine
kubectl get pods -l app=my-app # 1초 간격으로 반복 실행
실제 출력:
== t+0s
my-deployment-5f9c57bbf7-2d7m8 1/1 Terminating ← 기존 3개 동시에 종료 중
my-deployment-5f9c57bbf7-c2kmt 1/1 Terminating
my-deployment-5f9c57bbf7-pbjt9 1/1 Terminating
== t+1s
my-deployment-6bf9d78567-4fltv 0/1 ContainerCreating ← 기존이 다 사라진 뒤 새 3개 생성
my-deployment-6bf9d78567-jjg48 0/1 ContainerCreating
my-deployment-6bf9d78567-z28qr 0/1 ContainerCreating
== t+2s
my-deployment-6bf9d78567-4fltv 1/1 Running
my-deployment-6bf9d78567-jjg48 1/1 Running
my-deployment-6bf9d78567-z28qr 1/1 Running
- 결과 읽기: 롤링 업데이트와 달리 기존 3개가 한꺼번에 사라진 뒤 새 3개가 생김. 그 사이(t+0s ~ t+2s)에는 요청을 받을 파드가 하나도 없음
업데이트를 하는 동안 client 파드에서 0.2초마다 서비스에 요청을 보내 성공·실패 횟수를 셈
# 0.2초 간격 요청을 60번 보내며 업데이트 실행 (업데이트는 2초 뒤 시작)
kubectl exec client -- sh -c 'ok=0; ng=0; i=0; while [ $i -lt 60 ]; do
if curl -s -m 1 -o /dev/null http://my-service:8080; then ok=$((ok+1)); else ng=$((ng+1)); fi
i=$((i+1)); sleep 0.2; done; echo "성공=$ok 실패=$ng"'
| 조건 | 실제 결과 | 해석 |
|---|---|---|
| Recreate | 성공=54 실패=6 | 모든 파드가 사라진 약 1~2초 동안 요청 실패 (다운타임) |
| RollingUpdate (기본 설정만) | 성공=59 실패=1 | 거의 무중단이지만 1건 실패 |
| RollingUpdate + readinessProbe + preStop | 성공=100 실패=0 | 완전 무중단 |
- 롤링 업데이트인데 왜 1건이 실패했을까?
- 새 파드: readinessProbe가 없으면 컨테이너가 시작되자마자 "준비됨"으로 간주 → nginx가 완전히 뜨기 전에 요청을 받을 수 있음
- 기존 파드: 파드 종료와 서비스 목록(iptables 규칙)에서 빠지는 것이 동시에 진행됨 → 규칙이 갱신되기 직전의 아주 짧은 순간에 이미 종료 중인 파드로 요청이 감
- 해결: 디플로이먼트의 template에 두 가지를 추가
# deployment.yaml - containers 항목에 추가
readinessProbe: # 진짜 응답할 수 있을 때만 요청 받기 (5.7.4)
httpGet:
path: /
port: 80
periodSeconds: 2
lifecycle:
preStop: # 종료 신호를 받으면 5초 기다렸다가 종료
sleep: # → 그 사이 서비스 목록에서 먼저 빠짐
seconds: 5
- 즉 롤링 업데이트는 "무중단의 필요조건"이지 충분조건은 아님. 헬스 체크(5.7)와 함께 써야 진짜 무중단이 됨
# 참고: 업데이트 전략과 세부 값 지정 방법
spec:
strategy:
type: RollingUpdate # 또는 Recreate
rollingUpdate:
maxUnavailable: 25% # 업데이트 중 줄어들 수 있는 최대 파드 수 (기본 25%)
maxSurge: 25% # 업데이트 중 추가로 늘어날 수 있는 최대 파드 수 (기본 25%)
5.10.5. 자동 빌드/배포를 지원하는 도구
| 도구 | 개발 | 특징 |
|---|---|---|
| Skaffold | 구글 | 쿠버네티스 특화. 프로그램 변경을 감지해 이미지 생성부터 배포까지 자동 수행 |
| 젠킨스 X (Jenkins X) | - | 쿠버네티스 전문 자동 빌드/배포 도구 |
| 스피내커 (Spinnaker) | 넷플릭스 | 자동 배포 도구. 배포 방법을 더욱 세밀하게 설정 가능 (카나리 배포 등) |
- 트리거 (Trigger): 자동화 절차를 시작시키는 이벤트 (예: git push)
- 업데이트 전략: 기존 파드를 새 파드로 바꾸는 방법. 재생성과 롤링 업데이트가 있음
- 재생성 (Recreate): 기존 파드를 모두 중지한 뒤 새 파드를 시작하는 전략
- 롤링 업데이트 (Rolling Update): 기존 파드를 줄이면서 새 파드를 늘리는 전략. 디플로이먼트의 기본값
- maxSurge / maxUnavailable: 롤링 업데이트 중 추가로 늘어날 수 있는 / 줄어들 수 있는 최대 파드 수
- 롤백 (Rollback): 이전 버전으로 되돌리는 것.
kubectl rollout undo - preStop 훅: 컨테이너가 종료되기 직전에 실행되는 동작
- 현재 쿠버네티스 배포 자동화는 GitOps 방식이 주류임. git 저장소의 매니페스트를 "기대하는 상태"로 두고, 클러스터 안의 도구가 이를 감시해 자동으로 맞춤 (5.9.1의 선언형 개념을 배포까지 확장한 것)
- 대표 도구: Argo CD, Flux (둘 다 CNCF 졸업 프로젝트)
- 빌드(CI) 쪽은 GitHub Actions, GitLab CI 등이 널리 쓰이며, 젠킨스 X는 원서 시점보다 사용이 크게 줄었음
- 실습 18의
preStop에 쓴sleep동작은 최신 버전에서 지원하는 문법임. 예전에는exec: command: ["sleep", "5"]처럼 컨테이너 안의 sleep 명령을 실행했음
5.11. 멀티 클라우드: 여러 클라우드에서 쿠버네티스 사용
5.11.1. 멀티 클라우드란
- 멀티 클라우드(Multi Cloud): 여러 클라우드를 결합해 구성한 환경
- 클라우드의 분류
| 구분 | 대상 | 예시 |
|---|---|---|
| 퍼블릭 클라우드 | 일반인 대상으로 제공 | 아마존 웹 서비스(AWS), 구글 클라우드 플랫폼(GCP) 등 |
| 프라이빗 클라우드 | 특정 개인이나 조직에 제공 | 사내 전용 클라우드 등 |
- 비슷한 말인 하이브리드 클라우드는 보통 퍼블릭 + 프라이빗(온프레미스) 조합을 가리킴
5.11.2. 멀티 클라우드의 필요성
- 클라우드 사업자는 서버 관리·운영의 노하우가 뛰어나지만, 예상치 못한 원인으로 장애가 발생할 수 있음
- 단일 클라우드 구성: 그 클라우드에 장애가 나면 시스템 전체를 사용할 수 없음 → 클라우드 자체가 단일 장애 지점 (→ 5.7.2)
- 멀티 클라우드 구성: 한 클라우드에 장애가 나도 해당 클라우드를 분리하면 시스템을 계속 사용할 수 있음
- 가용성에 대한 요구가 커지면서 멀티 클라우드의 필요성도 점점 커지고 있음
- 그 밖의 이유: 특정 사업자에 묶이는 것(벤더 종속) 방지, 서비스별로 더 싸거나 좋은 클라우드 선택
단일 클라우드 vs 멀티 클라우드:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[단일 클라우드] [멀티 클라우드]
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 클라우드 A │ ✗ 장애 │ 클라우드 A │ │ 클라우드 B │
│ 시스템 │ │ 시스템 │ │ 시스템 │
└──────────┘ └──────────┘ └──────────┘
│ ✗ 장애 → 분리 │
▼ ▼
서비스 전체 중단 B에서 서비스 계속
5.11.3. 쿠버네티스를 사용한 멀티 클라우드
- 각 회사의 퍼블릭 클라우드 서비스는 공통점이 많지만 차이점도 있음
- 예: AWS Lambda용 프로그램을 GCP의 서버리스 서비스 클라우드 함수(Cloud Functions) 로 옮기려면 프로그램 변경이 필요하고 배포 매개변수도 달라짐
- 설계·배포 절차를 고안하면 해결할 수 있지만 개발·운영이 복잡해짐
- 쿠버네티스를 사용하면
- 서로 다른 클라우드를 같은 절차(같은 매니페스트, 같은 kubectl) 로 조작할 수 있음
- 클라우드 간 차이를 절충해 개발·운영 복잡성을 줄임
- → 멀티 클라우드를 더욱 쉽게 구축할 수 있음
- 실제로 이 노트에서 Docker Desktop에 적용한
deployment.yaml,service.yaml은 EKS(AWS), GKE(GCP), AKS(Azure)에서도 그대로 동작함
쿠버네티스가 클라우드 간 차이를 흡수:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[같은 매니페스트 + kubectl]
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌──────────┐
│ EKS │ │ GKE │ │ 온프레미스 │
│ (AWS) │ │ (GCP) │ │ 쿠버네티스 │
└────────┘ └────────┘ └──────────┘
클라우드마다 다른 부분은 쿠버네티스 아래로 감춰짐
kubectl은 컨텍스트(context) 를 바꿔 가며 여러 클러스터를 조작함. 실습 처음에 본 kubectl config get-contexts의 목록이 바로 그것
kubectl config get-contexts # 등록된 클러스터 목록 (* = 현재)
kubectl config use-context docker-desktop # 조작할 클러스터 전환
멀티 클라우드 환경이라면 aws-prod, gcp-prod처럼 컨텍스트를 여러 개 두고 같은 매니페스트를 각각 적용할 수 있음. 엉뚱한 클러스터에 적용하는 실수를 막으려면 명령 전 현재 컨텍스트를 확인하는 습관이 중요함
5.11.4. 멀티 클라우드 구축을 지원하는 도구
- Rancher: Rancher Labs가 개발한 컨테이너 관리 플랫폼
- 주요 퍼블릭 클라우드에 쿠버네티스 클러스터를 구축할 수 있음
- 그 외 기능
- 자격 증명의 중앙 관리
- 접근 제어 및 보안 정책 적용
- 단일 화면에서 여러 클러스터 상태 표시
- 멀티 클라우드 (Multi Cloud): 여러 클라우드를 결합해 구성한 환경
- 퍼블릭 클라우드: 일반인 대상으로 제공되는 클라우드 (AWS, GCP, Azure 등)
- 프라이빗 클라우드: 특정 개인이나 조직 전용으로 제공되는 클라우드
- 벤더 종속 (Vendor Lock-in): 특정 사업자의 고유 기능에 묶여 다른 곳으로 옮기기 어려워지는 상태
- 컨텍스트 (Context): kubectl이 어느 클러스터에 어떤 사용자로 접속할지 묶어 둔 설정
- Rancher: 여러 클라우드의 쿠버네티스 클러스터를 구축·관리하는 컨테이너 관리 플랫폼
- Rancher Labs는 2020년 SUSE에 인수되어 현재 Rancher는 SUSE가 개발함
- 그 밖의 멀티 클러스터 관리 도구: Google Anthos(GKE Enterprise), Azure Arc, Cluster API(클러스터 생성 자체를 선언형으로 관리), Karmada(여러 클러스터에 워크로드 분산) 등
5.12. 쿠버네티스의 아키텍처: 각 노드에서 동작하는 구성 요소
5.12.1. 쿠버네티스 아키텍처
- 쿠버네티스에는 마스터와 워커라는 두 유형의 노드가 있음 (→ 5-1의 5.2.6)
- 사용자는
kubectl등으로 마스터에게 명령을 보내고, 마스터는 명령에 따라 워커를 조작함 - 마스터와 워커는 각자 다양한 구성 요소(컴포넌트) 를 실행하며, 이들을 연동해 오케스트레이션을 수행함
- 마스터(컨트롤 플레인) = 홀 매니저 사무실. 주문을 받고(apiserver), 장부에 적고(etcd), 어느 주방에 맡길지 정하고(scheduler), 주문대로 나오는지 계속 확인함(controller-manager)
- 워커 = 주방. 주방장(kubelet)이 주문서(PodSpec)대로 요리(컨테이너)를 만들고, 서빙 담당(kube-proxy)이 손님 요청을 알맞은 테이블로 안내함
# 이 클러스터의 노드와 역할
kubectl get nodes
# 실제 출력:
# NAME STATUS ROLES AGE VERSION
# desktop-control-plane Ready control-plane 14d v1.36.1 ← 마스터 (컨트롤 플레인)
# desktop-worker Ready <none> 14d v1.36.1 ← 워커
# desktop-worker2 Ready <none> 14d v1.36.1 ← 워커
kubectl describe node desktop-control-plane | grep Taints
# 실제 출력:
# Taints: node-role.kubernetes.io/control-plane:NoSchedule
- 마스터에는
NoSchedule테인트가 걸려 있어 일반 파드가 올라가지 않음 → 지금까지 실습한 파드가 모두desktop-worker,desktop-worker2에만 배치된 이유 (실습 9의1 node(s) had untolerated taint(s)도 이것)
5.12.2. 쿠버네티스 클러스터를 조작하는 방법
| 방법 | 형태 | 특징 |
|---|---|---|
| kubectl 명령어 | 명령줄 인터페이스(CLI) | 명령을 실행해 클러스터를 조작 |
| 대시보드 | 웹 기반 GUI | 클러스터 조작 + 파드 CPU 사용률 등 통계를 그래프로 표시 |
| 쿠버네티스 API | API | 마스터에 요청을 보내 조작. 프로그램에서 접근할 때 적합 |
- kubectl과 대시보드도 내부적으로는 쿠버네티스 API를 사용함 → 모든 조작은 결국 API로 모임
# -v=6: kubectl이 보내는 HTTP 요청을 로그로 보여 줌
kubectl get pods -l app=my-app -v=6
실제 출력 (로그 줄):
"Response" verb="GET" url="https://127.0.0.1:62824/api/v1/namespaces/default/pods?labelSelector=app%3Dmy-app&limit=500" status="200 OK"
- 결과 읽기:
kubectl get pods -l app=my-app은 사실 API 서버에 HTTP GET 요청을 보내는 것/api/v1/namespaces/default/pods= default 네임스페이스의 파드 목록labelSelector=app%3Dmy-app= 레이블 셀렉터app=my-app(%3D는=의 URL 인코딩)
# kubectl 없이 같은 API를 직접 호출해 보기 (결과는 JSON)
kubectl get --raw "/api/v1/namespaces/default/pods?labelSelector=app%3Dmy-app"
# 실제 출력 (앞부분):
# {"kind":"PodList","apiVersion":"v1","metadata":{"resourceVersion":"92475"},
# "items":[{"metadata":{"name":"my-deployment-7f7c74cb79-9xsl5", ... "labels":{"app":"my-app", ...
- kubectl이 보여 주던 표는 이 JSON을 보기 좋게 정리한 것임. 프로그램에서는 이 API를 직접 부르면 됨
- 공식 웹 UI였던 Kubernetes Dashboard는 유지보수가 종료(아카이브)되었고, SIG UI는 대안으로 Headlamp를 권장함. 그 밖에 데스크톱 도구 Lens, 터미널 UI k9s 등도 많이 쓰임. Docker Desktop 자체 화면에서도 파드·서비스 목록을 볼 수 있음
5.12.3. 마스터의 구성 요소
| 구성 요소 | 역할 |
|---|---|
| kube-apiserver | 사용자와 워커가 클러스터와 상호 작용하는 엔드포인트(창구) 제공 |
| etcd | 데이터를 저장하는 키-값 저장소. 여러 노드로 구성되어 일부가 고장 나도 데이터 손실·불일치가 없음 |
| kube-scheduler | 파드를 시작할 노드를 계획. 필요한 리소스 등 다양한 조건을 고려해 최적의 노드 결정 |
| kube-controller-manager | 디플로이먼트 컨트롤러 등 여러 컨트롤러의 실행·정지를 관리해 클러스터를 정상 상태로 유지 |
- kube-apiserver와 etcd의 관계: 프론트엔드와 백엔드
- kube-apiserver는 데이터를 직접 갖지 않고, etcd를 통해 데이터를 관리함
- 다른 구성 요소는 etcd에 직접 접근하지 않고 반드시 kube-apiserver를 거침
# kube-system 네임스페이스 = 쿠버네티스 자신의 구성 요소가 사는 곳
kubectl get pods -n kube-system \
-o custom-columns=NAME:.metadata.name,OWNER:.metadata.ownerReferences[0].kind,NODE:.spec.nodeName
실제 출력:
NAME OWNER NODE
etcd-desktop-control-plane Node desktop-control-plane ┐
kube-apiserver-desktop-control-plane Node desktop-control-plane │ 마스터 구성 요소
kube-controller-manager-desktop-control-plane Node desktop-control-plane │ (5.12.3)
kube-scheduler-desktop-control-plane Node desktop-control-plane ┘
kube-proxy-gpv8r DaemonSet desktop-worker ┐ 워커 구성 요소 (5.12.4)
kube-proxy-hdbpr DaemonSet desktop-control-plane │ 노드마다 1개씩
kube-proxy-jvtsc DaemonSet desktop-worker2 ┘
kindnet-kg5rv DaemonSet desktop-worker ┐ CNI 플러그인
kindnet-kk4jr DaemonSet desktop-control-plane │ (5-1의 플라넬과
kindnet-n7d4w DaemonSet desktop-worker2 ┘ 같은 역할)
coredns-589f44dc88-jv6r9 ReplicaSet desktop-control-plane ┐ 클러스터 DNS
coredns-589f44dc88-pxbhk ReplicaSet desktop-control-plane ┘ (실습 1의 이름 확인)
- 결과 읽기
- 마스터의 4가지 구성 요소가 모두 컨트롤 플레인 노드에서 파드로 돌고 있음
- OWNER가
Node인 것은 스태틱 파드: API 서버가 뜨기도 전에 kubelet이 노드의 설정 파일을 읽어 직접 띄운 파드 (API 서버 자신을 API 서버로 만들 수는 없으니까) - kube-proxy는 데몬셋으로 모든 노드에 하나씩 배치됨
- coredns 2개는 레플리카셋으로 이중화되어 있음 (5.7.2)
# etcd 안에 실제로 무엇이 저장되어 있는지 키 목록 보기
kubectl exec -n kube-system etcd-desktop-control-plane -- etcdctl \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
get /registry/ --prefix --keys-only | grep "default/my-"
# 실제 출력:
# /registry/deployments/default/my-deployment
# /registry/endpointslices/default/my-service-4nkk7
# ...
- 결과 읽기: 우리가
kubectl apply로 만든 디플로이먼트, 서비스 등이 etcd에/registry/<종류>/<네임스페이스>/<이름>형태의 키로 저장되어 있음. 클러스터의 "기대하는 상태"가 모두 여기에 있음 - 이 학습용 클러스터는 etcd가 1개(
member list로 확인)지만, 운영 환경에서는 보통 3개 또는 5개로 구성해 일부가 고장 나도 데이터를 지킴 (과반수가 살아 있어야 동작하므로 홀수로 구성)
# 리더 선출: 컨트롤러 매니저·스케줄러를 여러 개 띄워도 일은 한 개만 하도록
kubectl get leases -n kube-system
# 실제 출력:
# NAME HOLDER
# kube-controller-manager desktop-control-plane_c1d0ded8-...
# kube-scheduler desktop-control-plane_399d5d47-...
- 마스터를 이중화하면 kube-controller-manager·kube-scheduler도 여러 개가 뜨는데, 동시에 일하면 충돌하므로 임대(Lease)를 가진 하나만 리더로 일하고 나머지는 대기함 (
--leader-elect=true옵션)
5.12.4. 워커의 구성 요소
- 워커는 컨테이너를 시작하는 데 필요한 컨테이너 런타임(원서: 도커, 크라이오(CRI-O) 등)과 두 에이전트 프로그램을 실행함
| 구성 요소 | 역할 |
|---|---|
| kubelet | 마스터에서 PodSpec(파드 정의)을 가져와 컨테이너 런타임 인터페이스(CRI) 로 컨테이너를 실행·정지·감시 (→ 5.7.4 헬스 체크) |
| kube-proxy | 마스터에서 네트워크 구성을 가져와 통신의 접근 중계·주소 변환 규칙을 변경 (→ 5.7.3 로드 밸런싱, 실습 2의 iptables 규칙) |
| 컨테이너 런타임 | 실제로 컨테이너를 실행 (이 클러스터는 containerd 2.3.1) |
kubectl get nodes -o wide
# 실제 출력 (일부 열):
# NAME ... OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
# desktop-worker ... Debian GNU/Linux 13 (trixie) 6.6.87.2-microsoft-standard-WSL2 containerd://2.3.1
- kubelet은 kube-system의 파드 목록에 보이지 않음 → kubelet은 파드가 아니라 노드에 직접 설치된 프로그램이기 때문 (파드를 띄우는 주체가 파드일 수는 없음)
쿠버네티스 아키텍처 전체 구성 (이 실습 클러스터 기준):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[kubectl] [대시보드] [프로그램]
└────────────┼────────────┘
│ 쿠버네티스 API (HTTPS, 실습 19)
▼
┌──────────────── desktop-control-plane (마스터) ────────────────┐
│ │
│ ┌────────────────┐ ┌──────────┐ │
│ │ kube-apiserver │ ←────→ │ etcd │ │
│ │ (프론트엔드) │ │ (백엔드) │ │
│ └───────┬────────┘ └──────────┘ │
│ │ │
│ ┌─────┴──────────────┐ │
│ ▼ ▼ │
│ ┌────────────────┐ ┌─────────────────────────┐ │
│ │ kube-scheduler │ │ kube-controller-manager │ │
│ │ 파드 배치 노드 결정 │ │ 컨트롤러 실행·관리 │ │
│ └────────────────┘ └─────────────────────────┘ │
└───────────────────────────────┬───────────────────────────────┘
│ PodSpec · 네트워크 구성
┌────────────────────┴────────────────────┐
▼ ▼
┌──────── desktop-worker ────────┐ ┌──────── desktop-worker2 ───────┐
│ [kubelet] [kube-proxy] │ │ [kubelet] [kube-proxy] │
│ │ CRI │ NAT 규칙 │ │ │ CRI │ NAT 규칙 │
│ ▼ ▼ │ │ ▼ ▼ │
│ [containerd] [iptables] │ │ [containerd] [iptables] │
│ │ │ │ │ │
│ [파드] [파드] │ │ [파드] [파드] │
└───────────────────────────────┘ └───────────────────────────────┘
새 디플로이먼트를 만들고, 그때 발생한 이벤트를 보고한 구성 요소(SOURCE) 와 함께 출력함
kubectl create deployment flow-demo --image=nginx:stable --replicas=1
kubectl get events --sort-by=.metadata.creationTimestamp \
-o custom-columns=SOURCE:.source.component,OBJECT:.involvedObject.kind,REASON:.reason,MESSAGE:.message \
| grep flow-demo
실제 출력 (발생 순서대로 정리):
SOURCE OBJECT REASON MESSAGE
deployment-controller Deployment ScalingReplicaSet Scaled up replica set flow-demo-7d6d49ff65 from 0 to 1
replicaset-controller ReplicaSet SuccessfulCreate Created pod: flow-demo-7d6d49ff65-pjnqd
default-scheduler Pod Scheduled Successfully assigned default/flow-demo-7d6d49ff65-pjnqd to desktop-worker2
kubelet Pod Pulled Container image "nginx:stable" already present on machine ...
kubelet Pod Created Container created
kubelet Pod Started Container started
파드가 실행되기까지 구성 요소의 연동 (위 출력과 대응):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 사용자 kubectl create deployment ...
│ (API 요청)
2. kube-apiserver 요청을 검사하고 etcd에 디플로이먼트 저장
│
3. kube-controller-manager
├─ deployment-controller "디플로이먼트에 RS가 없네" → RS 생성 (ScalingReplicaSet)
└─ replicaset-controller "RS에 파드가 0개네" → 파드 생성 (SuccessfulCreate)
│ 이 시점의 파드는 아직 노드가 정해지지 않은 상태
4. kube-scheduler 노드를 골라 파드에 기록 (Scheduled → desktop-worker2)
│
5. kubelet "내 노드에 배정된 파드가 있네" → containerd로 실행 (Pulled → Created → Started)
(desktop-worker2)
│
6. kube-proxy 파드가 서비스 대상이면 iptables 규칙 갱신
- 결과 읽기
- 각 구성 요소는 서로 직접 명령하지 않음. 모두 kube-apiserver를 지켜보다가(watch) 자기 몫의 변화가 생기면 일을 하고 결과를 다시 API 서버에 기록함
- 그래서 한 구성 요소가 잠시 멈춰도 다시 살아나면 밀린 일을 이어서 처리할 수 있음 → 쿠버네티스가 튼튼한 이유
already present on machine: 이 노드에는 실습 13 때 받은 이미지가 남아 있어 다시 받지 않음 (실습 10의 13초와 비교)
- kube-apiserver: 클러스터 조작의 유일한 창구. 모든 구성 요소가 이를 통해 통신
- etcd: 클러스터의 모든 상태를 저장하는 분산 키-값 저장소
- kube-scheduler: 새 파드를 어느 노드에서 실행할지 결정하는 스케줄러
- kube-controller-manager: 디플로이먼트 컨트롤러 등 각종 컨트롤러를 실행·관리하는 구성 요소
- PodSpec: 파드에 포함할 컨테이너, 이미지, 리소스 등을 정의한 명세 (매니페스트의
spec부분) - CRI (Container Runtime Interface): kubelet이 컨테이너 런타임을 조작하기 위한 표준 인터페이스
- kube-system: 쿠버네티스 자신의 구성 요소가 실행되는 네임스페이스
- 스태틱 파드 (Static Pod): API 서버를 거치지 않고 kubelet이 노드의 설정 파일로 직접 띄우는 파드
- 데몬셋 (DaemonSet): 모든(또는 지정한) 노드에 파드를 하나씩 배치하는 리소스
- 리더 선출 (Leader Election): 같은 구성 요소가 여러 개 떠 있을 때 하나만 일하도록 정하는 방식
- "마스터"는 현재 공식 용어로 컨트롤 플레인(Control Plane) 이라고 함. 실습에서도 노드 역할이
control-plane으로 표시됨 (→ 5-1의 갱신 노트) - 워커의 컨테이너 런타임으로 도커는 더 이상 직접 사용하지 않음. kubelet이 도커를 쓰기 위한 중계 계층(dockershim)이 v1.24에서 제거되어, 현재는 containerd 또는 CRI-O 같은 CRI 런타임을 사용함. Docker Desktop의 쿠버네티스조차 실습에서 본 것처럼
containerd를 씀 (도커로 빌드한 이미지는 OCI 표준이므로 그대로 실행 가능) - 원서의 kube-proxy 설명("파드와 파드가 통신할 때")은 정확히는 서비스를 거치는 통신의 규칙을 관리하는 것임. 파드 간 직접 통신은 CNI 플러그인(5-1의 플라넬, 이 실습의 kindnet 등)이 담당
- 클라우드 환경에서는 컨트롤 플레인에 클라우드 로드 밸런서·노드 연동을 담당하는 cloud-controller-manager도 함께 동작함
실습 정리하기
# 이 노트에서 만든 리소스 삭제
kubectl delete deployment my-deployment
kubectl delete service my-service
kubectl delete pod client resource-demo
kubectl delete hpa my-hpa # 만들었다면
kubectl get all
# 실제 출력 (정리 후):
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 14d ← 기본 서비스만 남음
핵심 요약
5-2장. 쿠버네티스의 가용성·스케일링·운영·아키텍처 요약:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[로드 밸런싱과 헬스 체크]
├─ 가용성: 원할 때 사용할 수 있는 정도, 가동률 = 정상 가동 시간 / 가동 시간
├─ 가용성 향상: 단일 장애 지점 제거 → 이중화 → LB·헬스 체크로 발견·교체 자동화
├─ 로드 밸런싱: 서비스 + kube-proxy가 iptables(또는 IPVS) NAT 규칙 관리
│ └─ 실습: iptables는 확률 1/3·1/2·나머지 규칙으로 무작위 균등 분배
├─ IPVS: 해시 테이블·커널 모드로 iptables보다 빠름 → 서비스 1만 개 초과 시 권장
└─ 헬스 체크: kubelet이 명령어·TCP·HTTP로 확인
├─ livenessProbe 실패 → 재시작 (실습: RESTARTS 1)
└─ readinessProbe 실패 → 서비스 대상에서 제외 (실습: READY 0/1, ready=false)
[스케일링]
├─ 요청 집중 시 증가(다운 방지), 한산할 때 감소(비용 절약)
├─ 메트릭이 임곗값을 상회·하회하면 증감 (과거 데이터로 연속 여부 판단하기도 함)
├─ 수평(개수) vs 수직(성능) → 수평이 처리 능력과 선형 관계에 가까움
├─ HPA: 파드 수평 스케일링, 기본 메트릭은 CPU 사용률 → metrics-server 필요
├─ VPA: request(하한)·limit(상한) 자동 조정, 별도 설치 (In-place Resize로 무중단 가능)
└─ CA: 클라우드에서 노드 수 증감, 파드가 리소스 부족으로 Pending일 때 증가
[주요 명령어]
├─ 매니페스트(YAML/JSON)에 기대하는 상태를 선언 → 절차는 쿠버네티스가 계획
├─ 공통 구조: apiVersion · kind · metadata · spec
├─ 레이블(키-값) + 레이블 셀렉터로 여러 객체를 일괄 선택 → 객체 간 연결고리
├─ apply(created/configured/unchanged) / get / describe / exec / delete
├─ 레플리카셋은 셀렉터에 맞는 기존 파드도 입양, 지우면 자동 복구
└─ 파드 → 레플리카셋(template) → 디플로이먼트(kind만 다름) → 서비스(selector로 연결)
[자동 빌드]
├─ 자동 빌드: 컴파일·라이브러리 설치·테스트 자동화, 결과물 = 아티팩트
├─ 자동 배포: 서버 업로드·DB 변경 등 릴리스 절차 자동화
├─ 3단계: 이미지 생성 → 레지스트리 등록 → 배포 업데이트 (git push 등을 트리거로)
├─ 업데이트 전략: 재생성(전체 중단, 혼재 없음) / 롤링 업데이트(무중단, 혼재, 기본값)
│ └─ 실습: Recreate 실패 6건, Rolling 1건, Rolling + readiness + preStop 0건
├─ 롤백: 이전 레플리카셋을 0개로 남겨 두었다가 rollout undo로 되돌림
└─ 도구: Skaffold(구글) · 젠킨스 X · 스피내커(넷플릭스)
[멀티 클라우드]
├─ 여러 클라우드(퍼블릭·프라이빗)를 결합한 환경
├─ 필요성: 한 클라우드 장애 시 분리해 서비스 유지 → 가용성 향상
├─ 쿠버네티스: 클라우드마다 다른 절차를 같은 방식으로 조작 → 복잡성 감소
└─ 도구: Rancher (클러스터 구축, 자격 증명 중앙 관리, 접근 제어, 단일 화면)
[아키텍처]
├─ 사용자 → 마스터(명령) → 워커(조작), 마스터에는 NoSchedule 테인트
├─ 조작 방법: kubectl · 대시보드 · 쿠버네티스 API (앞의 둘도 내부적으로 API 사용)
├─ 마스터: kube-apiserver(창구) · etcd(저장소, 백엔드) · kube-scheduler(배치) · kube-controller-manager(컨트롤러)
│ └─ 실습: kube-system에 스태틱 파드로 실행, etcd에 /registry/... 키로 저장
└─ 워커: 컨테이너 런타임(containerd) · kubelet(PodSpec → CRI로 실행·감시) · kube-proxy(주소 변환 규칙)
└─ 실습: 디플로이먼트 생성 시 deployment → replicaset → scheduler → kubelet 순서로 이벤트 발생
참고 자료
관련 자료:
- 서비스(Service): https://kubernetes.io/docs/concepts/services-networking/service/
- 가상 IP와 서비스 프록시(kube-proxy 모드): https://kubernetes.io/docs/reference/networking/virtual-ips/
- 서비스와 파드용 DNS: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/
- 프로브(Liveness, Readiness, Startup): https://kubernetes.io/docs/concepts/configuration/liveness-readiness-startup-probes/
- 프로브 설정 실습: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
- Horizontal Pod Autoscaling: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/
- metrics-server: https://github.com/kubernetes-sigs/metrics-server
- Vertical Pod Autoscaler: https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler
- Cluster Autoscaler: https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler
- 파드와 컨테이너의 리소스 관리(requests/limits): https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
- 파드 QoS 클래스: https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/
- 컨테이너 리소스 In-place 변경: https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/
- 레이블과 셀렉터: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
- kubectl 레퍼런스: https://kubernetes.io/docs/reference/kubectl/
- 디플로이먼트 업데이트 전략: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy
- 컨테이너 라이프사이클 훅(preStop): https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/
- Skaffold: https://skaffold.dev/
- Spinnaker: https://spinnaker.io/
- Argo CD: https://argo-cd.readthedocs.io/
- Rancher: https://www.rancher.com/
- kubectl 컨텍스트 설정: https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/
- 쿠버네티스 컴포넌트: https://kubernetes.io/docs/concepts/overview/components/
- 스태틱 파드: https://kubernetes.io/docs/tasks/configure-pod-container/static-pod/
- etcd: https://etcd.io/